PHP PDO 的单例替代方案

Rex*_*xha 3 php mysql pdo

这是我用来连接到我的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.
     }
}
Run Code Online (Sandbox Code Playgroud)

Ger*_*iks 7

使用单例模式(或反模式)被认为是不好的做法,因为它使得测试你的代码非常困难,而且依赖关系非常复杂,直到项目在某个时候变得难以管理。每个 php 进程只能有一个固定的对象实例。在为您的代码编写自动化单元测试时,您需要能够将要测试的代码使用的对象替换为行为可预测的测试替身。当您要测试的代码使用单例时,您不能用测试替身替换它。

组织对象(例如您的数据库对象和其他使用数据库的对象)之间的交互的最佳方法(据我所知)是反转依赖的方向。这意味着您的代码不是从外部源请求它需要的对象(在大多数情况下是全局对象,如代码中的静态“get_instance”方法),而是从外部获取其依赖对象(它需要的对象)在它需要之前。通常你会使用一个来自 symfony 项目的依赖注入管理器/容器来组合你的对象。

使用数据库对象的对象将在构造时注入它。它可以通过 setter 方法或在构造函数中注入。在大多数情况下(不是全部),最好在构造函数中注入依赖项(您的 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']);
Run Code Online (Sandbox Code Playgroud)

注意不同的类是如何相互不了解的。它们之间没有直接的依赖关系。这是通过不需要构造函数中的实际类来完成的,而是需要提供所需方法的接口。

这样你就可以随时为你的类编写替换,只需在依赖注入容器中替换它们。您不必检查整个代码库,因为替换只需实现所有其他类使用的相同接口。您知道一切都会继续工作,因为使用旧类的每个组件只知道接口并且只调用接口已知的方法。

PS:请原谅我对 symfony 项目的不断引用,这正是我最习惯的。其他项目如 Drupal、Propel 或 Zend 可能也有这样的概念。