【问题标题】:PDO and dependency injectionPDO 和依赖注入
【发布时间】:2017-01-25 21:55:31
【问题描述】:

我正在尝试找出在不使用单例方法的情况下将 PDO 与其他类一起使用的最佳方法。我已经在 * 上搜索了几十个问题,但我仍然不清楚如何去做。我知道显然依赖注入是要走的路,但我不太确定我是否理解它。这就是我想出的。

class MyPDO extends PDO {

    public function run($sql, $args = NULL) {
        $stmt = $this->prepare($sql);
        $stmt->execute($args);
        return $stmt;
    }

    public function addNew($table, $values) {
        $this->run('INSERT INTO ' . $table . ' (first_name) VALUES (?)', $values);
    }
}

class User {

    private $database = null;

    public function __construct(Database $database) {
        $this->database = $database;
    }

    public function register($user) {
        $this->database->addNew('users', $user);
    }

}

$pdo = new MyPDO("mysql:host=".DB_HOST.";dbname=".DB_NAME,DB_USER,DB_PASS);
$user = new User($pdo);
$user->register(array('name'));

我不确定这是否是一个好方法,或者我是否离基地很远。应该像现在这样在 MyPDO 类内部还是外部建立连接?另外,我想知道将用户插入数据库是否应该像现在一样在 MyPDO 类中,或者我是否应该在 User 类中创建一个函数以插入数据库。任何帮助表示赞赏。

【问题讨论】:

  • 一般来说,是的,这是 DI 的正确方法。不过,我强烈质疑extend PDO 的必要性,您的课程与普通的PDO 连接相比几乎没有增加任何东西。
  • 我从这个网站得到了包装的想法:phpdelusions.net/pdo/pdo_wrapper 这家伙似乎知道他在说什么,所以我想我应该使用那个包装。我应该只创建一个不扩展 PDO 的类,然后在其中仍然具有运行/查询功能吗?
  • IMO 你应该使用普通的 PDO 对象除非并且直到你有充分的理由来抽象它。 不要仅仅因为你认为它可能是一个包装器就创建一些包装器好主意,但实际上并没有确定它可以解决的具体问题。
  • 我不太清楚你的意思。是否有任何在其中具有数据库功能的类(如查询)被视为包装器?你是说我不应该在一个类中建立数据库连接,而是像我的示例中那样将 DB 变量传递给 User 类,然后从那里运行所有查询?
  • @deft 从根本上说,我赞赏你尝试走 DI 之路。现在,为了搞清楚,当你扩展一个类(在这种情况下是 PDO)时,你并没有包装它。如果您创建包装器(例如明确说明为准备好的语句选择正确的数据类型、关闭(设置为 null)stmt 对象等),则可以简化有关 PDO 的许多事情。但是,要创建真正的 PDO 包装器,您必须将 PDO 对象注入到包装类中。然后,在实例化时将其注入您的user 或其他类。我制作了一个 PDO 包装类,它让生活变得更轻松。

标签: php mysql pdo dependency-injection


【解决方案1】:

我就是那个编写 MyPDO“包装器”的人。回答您的问题,无需大惊小怪:

我不确定这是否是一个好方法,或者我是否离基地很远。

是的,很好。

应该像现在这样在 MyPDO 类内部还是外部进行连接?

最好在内部进行,因为除了建立连接之外,您还必须添加一些配置选项

如果将用户插入数据库应该像现在一样在 MyPDO 类中

绝不 - 不!
试想一下,如果你有十几节课会是什么样子!

原来如此

class MyPDO extends PDO {

    public function __construct($dsn, $username, $password, $options) {
        $default_options = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ];
        $options = array_merge($default_options, $options)
        parent::__construct($dsn, $username, $password, $options);
    }

    public function run($sql, $args = NULL) {
        $stmt = $this->prepare($sql);
        $stmt->execute($args);
        return $stmt;
    }
}

class User {

    private $database = null;
    private $table = "users";

    public function __construct(Database $database) {
        $this->database = $database;
    }

    public function register($user_data) {
        $this->database->run('INSERT INTO ' . $this->table . ' (first_name) VALUES (?)', $user_data);
    }
}

$pdo = new MyPDO("mysql:host=".DB_HOST.";dbname=".DB_NAME,DB_USER,DB_PASS);
$user = new User($pdo);
$user->register(array('name'));

【讨论】:

  • 将 PDO 对象注入 MyPDO 怎么样?
  • 感谢您的帮助@Your Common Sense。我想我会继续这样做,除非你能推荐一个更好的选择。
  • 例如,不确定依赖注入容器是否更好甚至是否必要。
  • 不是最灵活的解决方案,但我想它可以工作。
  • 怎么不灵活?只是好奇。
【解决方案2】:

我使用的是 PHP 5.6.x这是作弊,而不是依赖注入,但同时它可能会对您有所帮助。至少您可以将 PDO 包装器注入其他对象(用户等)。此外,此代码假定您希望将存储过程与准备好的语句一起使用。

abstract class Storage  //Program to an interface. Not an implementation.
{

}

abstract class Database extends Storage  //Program to an interface. Not an implementation.
{
    protected $pdo  = null;
    protected $stmt = null;

    public function __construct(PDO $pdo)
    {
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->pdo = $pdo;
    }

    public function __destruct() 
    {   
       $this->pdo  = null;
       $this->stmt = null;
       unset($this->pdo, $this->stmt);
    }

    protected function getDSN()
    {
        return "{$this->rdms}:host={$this->host};port={$this->port};dbname={$this->dbName};charset={$this->charset};"
    }

    /* Place all wrapping methods here, etc. */

    private function validatePDOStmtObject(PDOStatement $stmt)
    {
        $this->stmt = $stmt;
        return true;
    }

    public function query($sp)
    {
        return $this->validatePDOStmtObject($this->pdo->query("CALL {$sp}"));
    }

    public function prepare($sp)
    {
         try 
         {            
            $this->validatePDOStmtObject($this->pdo->prepare("CALL {$sp}"));
         }
         catch (\PDOException $pdoEx) 
         {
             throw new \RuntimeException("Failed to prepare query / stored procedure.<br>" . print_r($this->pdo->errorInfo(), true) . $pdoEx->getTraceAsString());
         }

         return;
    }

    public function bindParam($parameter, $variable)
    {
        $dataType = null;

        if(is_string($variable))
        {
            $dataType = PDO::PARAM_STR;
        }
        elseif(is_int($variable))
        {
            $dataType = PDO::PARAM_INT;
        }
        elseif(is_bool($variable))
        {
            $dataType = PDO::PARAM_BOOL;
        }
        elseif($variable === null)
        {
            $dataType = PDO::PARAM_NULL;
        }
        else
        {
            $dataType = PDO::PARAM_LOB;
        }

        //Where the actual binding takes place.
        if(!$this->stmt->bindParam($parameter, $variable, $dataType))
        {
            throw new \RuntimeException("Failed to bind paramenter $parameter" . print_r($this->stmt->errorInfo(), true));
        }

        return;
    }

    public function execute()
    { 
        $flag = false;

        try 
        {
            $this->stmt->execute();
            $flag = true;
        }    
        catch (\PDOException $pdoEx) 
        {
            error_log($pdoEx->getTraceAsString() . '<br><br>' . print_r($this->stmt->errorInfo(), true));
            //echo $pdoEx->getTraceAsString() . '<br><br>' . print_r($this->stmt->errorInfo(), true);
        }

        return $flag;
    }

    public function fetch()
    {
        return $this->stmt->fetch();
    }

    public function fetchColumn()
    {
        return $this->stmt->fetchColumn();
    }

    public function fetchAll()
    {
        $rows = $this->stmt->fetchAll();
        $this->clearRowsets();
        $this->stmtClose();
        return $rows;
    }

    public function clearRowsets()
    {
        if(isset($this->stmt))
        {
            while($this->stmt->fetch()) 
            {
                if(!$this->stmt->nextRowset())
                {
                    break;
                }
            }
        }

        return;
    }

    public function stmtClose()
    {
        $this->stmt = null;
        return;
    }

    public function closeCursor()
    {
        $this->stmt->closeCursor();
        return;
    }

    public function close()
    {
        $this->pdo = null;
        return;
    }

    public function startTransaction()
    {
        if($this->pdo->beginTransaction())
        {
            //'<br>Starting PDO/MySQL transaction.<br>';
            error_log('<br>Starting PDO/MySQL transaction.<br>');
        }
        else
        {
            throw new \RuntimeException('Failed to prepare the PDO statement.<br>' . print_r($this->pdo->errorInfo(), true));
        }

        return;
    }

    public function commit()
    {
        if($this->pdo->commit())
        {
            //echo '<br>Committing datbase changes.<br>';
            error_log('<br>Committing datbase changes.<br>');
        }
        else
        {
            throw new \RuntimeException('PDO was unable to commit the last statement.<br>' . print_r($this->pdo->errorInfo(), true));
        }

        return;
    }

    public function rollback()
    {
        if($this->pdo->rollback())
        {
            //echo '<br>Rolling back datbase changes.<br>';
            error_log('<br>Rolling back datbase changes.<br>');
        }
        else
        {
            throw new \RuntimeException('PDO was unable to rollback the last statement.<br>' . print_r($this->pdo->errorInfo(), true));
        }

        return;
    }
}

最后,您可以设计一个具体的子类。

class MySQL extends Database  //Now, build you concrete implementation.
{
    protected $rdms    = 'mysql';
    protected $host    = null;
    protected $port    = null;
    protected $dbName  = null;
    protected $charset = null;

    private static $instance = null;

    // PURE DI would start here by injecting a PDO object.
    // However, the PDO object must be configured, too.
    // It is possible to do PURE PDO DI.

    public function __construct($host = null, $port = null, $dbName = null, $charset = null)
    {
        require_once 'MySQLCreds.php'; 

        //$host, $port, $dbName, and $charset can be stored in an
        //include, or be supplied as arguments to the MySQL constructor.

        $this->host    = $host;
        $this->port    = $port;
        $this->dbName  = $dbName;
        $this->charset = $charset;

        parent::__construct(new PDO($this->getDSN(), $username, $password, $options));

        // Starting here with DI is cheating, but it gets you on the
        // right track for now. Database::getDSN() is used to dynamically
        // construct the DSN.
    }

    /* Destructor */
    public function __destruct() 
    {   
        $this->rdms    = null;
        $this->host    = null;
        $this->port    = null;
        $this->dbName  = null;
        $this->charset = null;
        unset($this->rdms, $this->host, $this->port, $this->dbName, $this->charset);
        parent::__destruct();
    }

    /* Only if you wanted to make a singleton. In that case,
       make the constuctor private.
    */
    public static function getInstance()
    {
       if(!isset(self::$instance))
       {
            self::$instance = new self();
       }

       return self::$instance;
    }
}

可能将其实例化为:

$user = new User(new MySQL())
        //Uses an include inside of the constructor.

这也是可能的。

$user = new User(new MySQL($host, $port, $dbName, $charset))
        //Externally supplied arguments.

有些人使用依赖注入器容器、工厂或单例。选择你的毒药。无论如何,尝试实现一个自动加载器并使用命名空间。注意:请注意此序列可能导致的PDOException。酌情使用try/catch

哦,顺便说一下,User 的构造函数可能看起来像这样。

abstract class Person
{
    protected $db = null;

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

class User extends Person
{
    public function __construct(Database $db)
    {
        parent::__construct($db)
    }
}

从技术上讲,我可以使用 Storage 的类型提示,但这需要使用公共接口方法填充 Storage 类,这些方法调用统一的具体实现方法(在 Storage 的子类中定义:数据库、XMLFile、文件等)。在某些情况下,这不是一个可能的多态解决方案。但是,您可以换出任何类型的数据库,它应该仍然可以工作。

【讨论】:

  • 怀疑?在本文中了解依赖注入的优势。 russellscottwalker.blogspot.com/2013/09/…
  • 我对依赖注入并不怀疑。我知道这是要走的路,我只是不清楚它完全是什么。我以为你说你的例子不是依赖注入?不幸的是,目前我的理解有点复杂。另外,感谢您的文章。
  • 如果您阅读了 cmets,您就会明白为什么从技术上讲这不是 DI。但99%是。您选择的答案在这方面并没有更好,但我的答案更灵活。也就是说,如果您将Database::prepareDatabase::query 方法更改为采用名为$sql 的参数,而不是假设使用存储过程,则会更加灵活。
【解决方案3】:

我昨天遇到了完全相同的问题。我认为使用依赖注入也是可行的方法。

我不能告诉你我的方式是否是最好的方式,但它对我来说非常好。

首先,您的课程 MyPDO 带走了很多 PDO 提供给您的功能 (See here)。

我这样解决了我的问题:

class Database {

    public $con;

    function __construct($dsn, $user, $password) {
        $this->connect($dsn, $user, $password);
    }

    private function connect($dsn, $user, $password) {

        try {

            $this->con = new PDO($dsn, $user, $password, array(
                PDO::ATTR_PERSISTENT => true
            ));

        } catch (PDOException $e) {
            echo 'Connection failed: ' . $e->getMessage();
        }
    }
}

class User {

    private $database;

    public function __construct(Database $database) {
        $this->database = $database;
    }

    public function register($user) {
        $stmt = $this->database->prepare("
            INSERT INTO user (first_name)
            VALUES (:first_name)
        ");

        $stmt->bindParam(':first_name', $first_name);
        $first_name = $user;

        $stmt->execute();
    }

}


$database = new Database("mysql:host=".DB_HOST.";dbname=".DB_NAME,DB_USER,DB_PASS);
$user = new User($database);
$user->register('name');

在我看来,应该在课堂内部建立联系,因为联系是课堂的重点。它还使您能够进行异常处理。

根据我的经验,最好在合适的类中编写所有语句。就像我在上面的代码中所做的那样。

希望对你有帮助。

【讨论】:

  • OP 的类实际上并没有从 PDO 中拿走任何东西,因为它 extends 它。另一方面,你的类甚至不起作用;您的代码将因Database has no prepare method 错误而失败... :)
  • 确实如此。但是你为什么要扩展已经有更好方法的东西。我在用户类中使用 prepare 方法。对于每个语句个体。 Like described here(本节第三个代码块)
  • 嗯,OP 似乎认为他们正在向已经 awesome 类添加更好的方法! ;-)(我也觉得这非常值得商榷。)
  • 说实话我真的不知道我在做什么哈哈。我刚刚从 phpdelusions.net 发现了这个想法。我想到的另一种方式就像@Wurstlaugen 所说的那样。我想知道为什么这得到了反对票?我上面贴的方法真的更好吗?
  • 另外,我看不到我的课程如何带走任何功能,因为就像@deceze 说它正在扩展它,而你链接我的示例没有。谢谢你们的帮助。