用于PHP PDO的Singleton替代方案

时间:2016-06-09 18:15:53

标签: 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.
     }
}

1 个答案:

答案 0 :(得分:2)

使用单例模式(或反模式)被认为是不好的做法,因为它会使代码非常难以测试,并且依赖性非常复杂,直到项目在某些时候难以管理。每个php-process只能有一个固定的对象实例。在为代码编写自动单元测试时,您需要能够使用以可预测方式运行的测试双重替换要测试的代码所使用的对象。当您要测试的代码使用单例时,则不能用测试双重替换它。

组织对象之间的交互(比如数据库对象和使用数据库的其他对象)的最佳方式(对我的知识)将是反转依赖关系的方向。这意味着您的代码不是从外部源请求它所需的对象(在大多数情况下是一个全局的,如代码中的静态'get_instance'方法),而是从外部获取它的依赖性对象(它需要的对象)在它需要之前。通常你会使用像this one from the symfony project这样的Depency-Injection Manager / Container来组合你的对象。

使用数据库对象的对象会在构造时注入它。它可以通过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']);

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

通过这种方式,您始终可以为类编写替换,并在dependencycy-injection容器中替换它们。您不必检查整个代码库,因为替换只需要实现所有其他类使用的相同接口。你知道一切都会继续工作,因为使用旧类的每个组件只知道接口,只调用接口已知的方法。

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