PHP PDO的单例替代方法

4
这是我用来连接我的MySQL数据库的类。 正如你所看到的,我使用了Singleton Pattern,但几乎每篇文章都说这是一种非常糟糕的模式。创建一个数据库连接类的最佳方法是什么?有更好的模式吗?
class DB extends PDO {

    function __construct() {
        try {
            parent::__construct('mysql:host=' . 'localhost' . ';dbname=' . 'kida', 'root', 'root', array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'");
            parent::setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch(PDOException $e) {
            echo $e->getMessage();
        }
    }

    public static function get_instance() {
        static $instance = false;
        if(!$instance) $instance = new self;
        return $instance; //returns pdo object.
     }
}

2
你有比较过你所做的和ORM(如Doctrine或Propel)的方式吗? - tadman
3
认为一个模式好坏纯属个人意见。某些意见比其他意见更好(例如,“谋杀是不好的”),但仍然只是一种意见。如果在你的特定情况下一个模式对你有效,那就这样吧。如果你发现自己使用了很多“不好”的模式,那么也许你应该重新评估你的设计,但在这些事情上并没有100%准确的好/坏评分标准。 - Marc B
3
一篇不错的文章:在像PDO这样用户友好的原始API上构建一个抽象类,就像断了铅笔一样毫无意义。 - JimL
1
只需将其传递到需要它的任何地方。$db = new PDO... function findUser($db, $id){ $db->query('按ID获取用户'); } - JimL
1
这取决于你的应用程序结构。许多请求/响应式应用程序(如Symfony)使用前端控制器,所有请求都发送到该控制器,它设置(引导)应用程序并将请求路由到应运行的任何代码。无论哪种方式,都只是为了找到请求的起始点,设置数据库连接并实例化注入所需内容的类。这被称为依赖注入,是一个值得一读的主题。 - JimL
显示剩余13条评论
1个回答

7
使用单例模式(或反模式)被认为是不好的实践,因为它使得测试代码变得非常困难,并且依赖关系非常复杂,导致项目在某些时候难以管理。你只能拥有一个每个php进程内的对象固定实例。当为您的代码编写自动化单元测试时,您需要能够替换要测试使用的对象与一个行为可预测的测试 double。当要测试的代码使用单例模式时,您就无法将其替换为一个测试double。
我所知道的最好的组织对象之间交互的方式(如您的数据库对象和其他使用数据库的对象)是反转依赖方向。这意味着您的代码不是从外部源(大多数情况下是您的代码中的静态“get_instance”方法)请求所需的对象,而是在需要之前从外部获得其依赖对象(所需的对象)。通常,您会使用一种依赖注入管理器/容器,例如来自Symfony项目的此类来组合您的对象。
使用数据库对象的对象将在构造函数中进行注入。它可以通过设置器方法或构造函数进行注入。在大多数情况下(并非所有情况),最好在构造函数中注入依赖项(您的db-object),因为使用db-object的对象永远不会处于无效状态。
例如:

interface DatabaseInterface
{
    function query($statement, array $parameters = array());
}

interface UserLoaderInterface
{
    public function loadUser($userId);
}

class DB extends PDO implements DatabaseInterface
{
    function __construct(
        $dsn = 'mysql:host=localhost;dbname=kida',
        $username = 'root',
        $password = 'root',
    ) {
        try {
            parent::__construct($dsn, $username, $password, array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'");
            parent::setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch(PDOException $e) {
            echo $e->getMessage();
        }
    }

    function query($statement, array $parameters = array())
    {
        # ...
    }
}

class SomeFileBasedDB implements DatabaseInterface
{
    function __construct($filepath)
    {
        # ...
    }

    function query($statement, array $parameters = array())
    {
        # ...
    }
}

class UserLoader implements UserLoaderInterface
{
    protected $db;

    public function __construct(DatabaseInterface $db)
    {
        $this->db = $db;
    }

    public function loadUser($userId)
    {
        $row = $this->db->query("SELECT name, email FROM users WHERE id=?", [$userId]);

        $user = new User();
        $user->setName($row[0]);
        $user->setEmail($row[1]);

        return $user;
    }
}

# the following would be replaced by whatever DI software you use,
# but a simple array can show the concept.


# load this from a config file
$parameters = array();
$parameters['dsn'] = "mysql:host=my_db_server.com;dbname=kida_production";
$parameters['db_user'] = "mydbuser";
$parameters['db_pass'] = "mydbpassword";
$parameters['file_db_path'] = "/some/path/to/file.db";


# this will be set up in a seperate file to define how the objects are composed
# (in symfony, these are called 'services' and this would be defined in a 'services.xml' file)
$container = array();
$container['db'] = new DB($parameters['dsn'], $parameters['db_user'], $parameters['db_pass']);
$container['fileDb'] = new SomeFileBasedDB($parameters['file_db_path']);

# the same class (UserLoader) can now load it's users from different sources without having to know about it.
$container['userLoader'] = new UserLoader($container['db']);
# or: $container['userLoader'] = new UserLoader($container['fileDb']);

# you can easily change the behaviour of your objects by wrapping them into proxy objects.
# (In symfony this is called 'decorator-pattern')
$container['userLoader'] = new SomeUserLoaderProxy($container['userLoader'], $container['db']);

# here you can choose which user-loader is used by the user-controller
$container['userController'] = new UserController($container['fileUserLoader'], $container['viewRenderer']);

Notice how the different classes no not know about each other. There are no direct depencies between them. This is done by not require the actual class in the constructor, but instead require the interface that provides the methods it needs.

That way you can always write replacements for your classes and just replace them in the depency-injection container. You do not have to check the whole codebase because the replacement just has to implement the same interface that is used by all other classes. You know that everything will continue to work because every component using the old class only knows about the interface and calls only methods known by the interface.

P.S.: Please excuse my constant references to the symfony project, it is just what i am most used to. Other project's like Drupal, Propel or Zend probably also have concepts like this.


抱歉,这个例子让我很困惑。我从未在PHP中使用过这些框架?是否有一个纯PHP的简单例子或者主要思路的例子呢?@ Gerrit - M1X
主要思想是将所有对象(如db-object)提供给从外部使用它们的其他对象。与在需要时从内部查找并获取db不同,您从外部查看并将db“提供”给其他对象(例如在构造函数中)。 - Gerrit Addiks
顺便说一下,我回答中的示例代码是纯PHP。我引用的其他类只是为了表达想法,并不存在。 - Gerrit Addiks

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