PHP的PDO实例作为私有静态属性

3
我正在尝试基于面向对象的方式设计我的网站,但是我在设计数据库连接时遇到了困难。目前,我正在一个抽象类Connector中创建一个私有静态PDO对象。显然,需要与数据库交互的任何内容都将扩展此类。我一直在考虑如何确保一个脚本中只有一个连接或PDO对象,因为有些页面将需要多个扩展Connector类的类。许多人似乎推荐使用单例模式来实现这个目的,但我目前的做法似乎也能达到相同的效果。
以下是我的当前代码。
abstract class Connector
{
    private static $dbh;

    public function __construct()
    {
        try
        {
            self::$dbh = new PDO(...);
            self::$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        }
        catch(PDOException $e)
        {
            die($e->getMessage());
        }
    }

    public function getDB()
    {
        return self::$dbh;
    }
}

然后任何子类都可以像这样使用它。

class Subclass extends Connector
{
    public function interactWithDB()
    {
        $stmt = $this->getDB()->prepare(...);
        // etc...
    }
}

从理论上讲,我想每个子类的实例应该始终访问同一个PDO实例。这段代码是否合理?我是否对静态属性有误解?这是不良设计/实践还是单例模式有更多优势?
如有不清楚之处,请留言。谢谢!
编辑:Connector类并非只用于保存PDO对象。其析构函数关闭连接(将其置为null),并包含isValueTaken等函数,用于检查值是否已存在于数据库中。它具有以下抽象函数。
abstract function retrieveData();
abstract function setData();

例如,我有一个扩展Connector的User类。它定义了setData()方法来在数据库中注册用户。我不知道这是否对响应产生影响。

8
很显然,任何需要与数据库交互的东西都会扩展这个类。——事实上并不明显。就像你需要使用勺子一样,并不需要成为勺子,对吧?提示:搜索依赖注入和委托。PS:http://pimple.sensiolabs.org/ (翻译可能有所不同,但尽量保持原意) - zerkms
1
我建议使用类似这种方法 - tereško
@zerkms pimple 实现了一个服务提供者,这是另一种反模式。而服务提供者并不是 DI 容器。 - tereško
1
@tereško:有没有任何“真正的”开源PHP DI容器可用? - zerkms
1
@tereško:我确实意识到了差异。我能够接受sf2服务容器的“限制”。 - zerkms
显示剩余2条评论
3个回答

7
显然,任何需要与数据库交互的内容都将扩展此类。 从面向对象的角度来看,这真的没有意义。当某个类扩展另一个类时,就意味着存在“is a”的关系。如果您走这条路,很难避免违反OCP,这是SOLID中的一个字母。
我一直在摇摆不定,如何确保脚本中只有一个连接或PDO对象,因为某些页面需要多个扩展Connector类的类。 简单!只需创建一个实例。
许多人似乎推荐使用Singleton模式来实现此目的,但我目前使用的方式似乎可以实现相同的功能。 像那样的许多人对面向对象原则一无所知。使用单例只会引入“花哨”的全局实例/状态

这段代码是否有意义,或者我对静态属性有误解?

说实话,这更多是对面向对象编程的误解。

这是不良的设计/实践吗?单例模式是否有更多的优势?

请参考上文。


你在面向对象编程中应该做的是将数据库连接注入到需要它的类中。这使得你的代码耦合度低,从而使你的代码更易于维护、测试、调试和灵活。
此外,我不太明白为什么需要为pdo连接创建一个数据库类,因为PDO API本身就是面向对象的。所以,除非你有真正的理由编写一个适配器(可能是这种情况,因为有一些情况),否则我会放弃它。
我的意见:€0.02

--

作为对您的编辑的回应:
Connector类不仅仅是用来保存PDO对象。它的析构函数会关闭连接(将其设置为null)。
通常情况下,没有必要关闭连接。一旦请求处理完毕,连接就会自动关闭(除非我们在谈论持久连接)。
此外,它包含了isValueTaken等函数,用于检查值是否已经存在于数据库中。这听起来需要另一个类来完成。
例如,我有一个User类扩展自Connector。它定义了setData()以在数据库中注册用户。我不知道这是否对回应有所影响。
不,我的观点依然存在。没有必要让用户从数据库继承而来。这听起来很奇怪。一个用户从数据库继承(我不想遇到那个人)。如果需要,您应该将数据库连接注入到User中。

我编辑了我的问题以包含更多信息。如果你的回答仍然适用,保留抽象类但将PDO对象作为参数传递给需要它的任何函数是否有意义? - shaqb4
除了试图在需要依赖注入时保持PDO实例在作用域内,您有什么关于如何使其可用的提示吗? - Phil
对于我的几乎所有项目,我都可以只保持“在范围内”。有时我会使用DIC。当我说DIC时,我并不是指将整个容器传递到每个地方并将其转换为服务定位器,而是指以一种方式进行线路连接,让我的东西为我工作。 - PeeHaa

3

前言

单例模式一般不受欢迎。你会被鸟龙攻击


你实际上想知道如何配置一个连接并使其全局可用。这通常称为全局状态。你可以使用容器类和静态方法来实现此目的。

以下是一个示例:

namespace Persistence\Connection\Config;

interface PDOConfig {
    public function getDSN();
    public function getUsername();
    public function getPassword();
    public function getDriverOptions();
}

class MySqlConfig implements PDOConfig {
    private $username;
    private $password;
    private $db;
    private $host = 'localhost';
    private $charset = 'utf8';

    public function __construct($username, $password, $db) {
        $this->username = $username;
        $this->password = $password;
        $this->db = $db;
    }

    // getters and setters, etc

    public function getDSN() {
        return sprintf('mysql:host=%s;dbname=%s;charset=%s',
            $this->host, $this->db, $this->charset);
    }

    public function getDriverOptions() {
        return [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_EMULATE_PREPARES => false,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ];
    }
}

namespace Persistence\Connection;

use Persistence\Connection\Config\PDOConfig;

class Registry {
    private static $connection;
    private static $config;

    public static function setConfig(PDOConfig $config) {
        self::$config = $config;
    }

    public static getConnection() {
        if (self::$connection === null) {
            if (self::$config === null) {
                throw new RuntimeException('No config set, cannot create connection');
            }
            $config = self::$config;
            self::$connection = new \PDO($config->getDSN(), $config->getUsername(),
                $config->getPassword(), $config->getDriverOptions());
        }
        return self::$connection;
    }
}

然后,在应用程序执行周期的某个时候(早期)

use Persistence\Connection\Config\MySqlConfig;
use Persistence\Connection\Registry;

$config = new MySqlConfig('username', 'password', 'dbname');
Registry::setConfig($config);

然后,您可以使用:
Registry::getConnection();

在代码中的任何位置使用该语句来检索PDO实例。


1
我知道有时候单例模式是个不好的词,但无论如何,你需要一些全局状态。你也可以使用注册表模式,但这真的取决于你自己。什么?不行,也不行。 - PeeHaa
1
OP正在询问关于面向对象编程(OOP)的问题。单例模式和注册表与OOP无关。是的,你是正确的,大多数框架也与OOP关系不大。 - PeeHaa
@zerkms 现在你让我挖掘代码,但我会找到的 :) - Phil
1
@Phil 一个带有“有意义的”静态注册表是单例。 - tereško
1
@tereško 我本来想说它不是单例,而是静态的、延迟加载的实例化器。然后我意识到,事实上,它就是一个单例 :) - Phil
显示剩余6条评论

1

如果有人在阅读这篇文章,使用Phil提供的上述代码片段,请记得在getDriverOptions()函数内PDO前加上一个反斜杠以引用全局命名空间。应该像这样:

public function getDriverOptions() {
      return [
          \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
          \PDO::ATTR_EMULATE_PREPARES => false,
          \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC
      ];
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接