【问题标题】:PHP dependency injection in extended class扩展类中的 PHP 依赖注入
【发布时间】:2014-06-05 10:49:09
【问题描述】:

我刚刚开始了解OOP,很抱歉,如果这个问题看起来有点过头了,那是我现在的感受。

我看过constructors in the PHP docs,但它似乎没有涵盖依赖注入。

我有一个名为 DatabaseLayer 的类,这个类只是创建到我的数据库的连接

//php class for connecting to database
/**
 * Class DatabaseLayer - connects to database via PDO
 */
class DatabaseLayer
{
    public $dbh; // handle of the db connexion
    /**
     * @param $config Config- configuration class
     */
    private function __construct(Config $config)
    {
        $dsn =  $config->read('db.dsn');
        $user = $config->read('db.user');
        $password = $config->read('db.password');
        $this->dbh = new \PDO($dsn, $user, $password);
    }

    public function getConnection()
    {
        if ($this->dbh)
        {
            return $this->dbh;
        }
        return false;
    }
}

Q1:我有private function __construct(Config $config) 我不确定我是否完全理解使用__construct(Config $config) 而不是仅仅使用__construct($config) 的原因 (Config $config) 是否会自动创建 $config 作为新的 Config 实例?

或者我必须执行以下操作:

$config = new Config();
$dbLayer = new DatabaseLayer($config);

我想扩展DatabaseLayer 类并包含与我的GameUser 相关的数据库进行交互的方法,这是我创建的另一个类,当我扩展DatabaseLayer 类时,我需要注入GameUser

我知道我的新类 DatabaseLayerUser 继承了 DatabaseLayer 类的方法和属性。

Q2:因为新的DatabaseLayerUser 类基于'DatabaseLayer' 类,它需要有Config 类,因为我使用了__construct(Config $config),这会自动获取它吗?

或者我必须将ConfigGameUser 都传递给DatabaseLayerUser

class DatabaseLayerUser EXTENDS DatabaseLayer
{

    private $config;    /** @var Config   */
    private $user;      /** @var GameUser */

    /**
     * @param Config    $config
     * @param GameUser  $user
     */
    private function __construct(Config $config, GameUser $user){
        $this->config = $config;
        $this->user = $user;
    }
    /**
     * profileExists - Checks to see if a user profile exists
     * internal @var $PDOQuery
     * @var $PDOStatement PDOStatement
     * @throws Exception - details of PDOException
     * @return bool
     */
    private function profileExists()
    {
        try{
            $PDOQuery = ('SELECT count(1) FROM userprofile_upl WHERE uid_upl = :uid');
            $PDOStatement = $this->dbh->prepare($PDOQuery);
            $PDOStatement->bindParam(':uid', $this->_userData->uid);
            $PDOStatement->execute();
            return $PDOStatement->fetchColumn() >0;
        }catch(PDOException $e){
            throw  new Exception('Failed to check if profile exists for '.$this->_userData->uid, 0, $e);
        }
    }
}`

Q3:我看到有一个parent::__construct(); 这是否意味着我应该使用:

private function __construct(Config $config, GameUser $user){
            parent::__construct($config);
            $this->user = $user;
        }

【问题讨论】:

  • Q1:只检查数据类型。

标签: php class oop dependency-injection extend


【解决方案1】:

我认为您在这里确实混淆了手段和目的。目标绝不是使用依赖注入本身,而是解决您遇到的编程问题。因此,让我们首先尝试更正一下类之间的依赖关系及其职责。

在我看来,这个练习最好从应用领域开始,从小处着手,如果要引入抽象,再进行重构。至少在学习或作为思想实验时。当更有经验并且在实施真实的事情时,提前几步可能更聪明。

所以现在我只是假设您正在制作一个在线游戏,其中包含由 GameUser 对象表示的不同用户,仅此而已。

  • GameUser 类的唯一职责应该是表示域数据和逻辑,即:它包含几个属性(比如usernamescore)和方法(比如incrementScore)这与应用程序域本身(游戏用户)有关。它应该负责实际的实现细节,最值得注意的是:它如何被持久化(写入文件/db/whatever)。这就是为什么负责存储自己的DatabaseLayerUser 可能是个坏主意。所以让我们从一个漂亮干净的GameUser 域类开始,并使用封装(私有属性以防止来自外部的篡改):

    class GameUser {
    
        private $_username;
        private $_score;
    
        public function __construct($username, $score) {
            $this->_username = $username;
            $this->_score = $score;
        }
    
        public function getUsername() { return $this->_username; }
        public function getScore() { return $this->_score; }
        public function incrementScore() {
            $this->_score++;
            return $this->_score;
        }
    }
    

    我们现在可以创建一个新的GameUser ($user = new GameUser('Dizzy', 100);),但我们不能持久化它。所以我们需要某种存储库,我们可以在其中存储这些用户并稍后再次获取它们。让我们这样称呼它:GameUserRepository。这是一个服务类。稍后当有不止一种类型的域对象和存储库时,我们可以创建一个 DatabaseLayer 类,将它们分组和/或充当外观,但我们现在从小处着手,稍后将重构。

  • 再一次,GameUserRepository 类的职责是允许我们获取和存储GameUser 对象,并检查给定用户名的配置文件是否存在。原则上,存储库可以将GameUser 对象存储在文件中或其他位置,但现在,我们在这里选择将它们保存在 SQL 数据库中(从小处着手,稍后重构,你会明白的。)

    GameUserRepository 的职责不是管理数据库连接。然而,它将需要一个数据库对象,以便将 SQL 查询传递给它。因此它委派了建立连接和实际执行它将创建的 SQL 查询的责任。

    代表团敲钟。依赖注入在这里发挥作用:我们将注入一个 PDO 数据库对象(服务)。我们将把它注入到构造函数中(即构造函数 DI 而不是 setter DI)。然后调用者负责弄清楚如何创建 PDO 服务,我们的存储库真的不在乎。

    class GameUserRepository {
    
        private $_db;
    
        public function __construct(PDO $db) {
            $this->_db = $db;
        }
    
        public function profileExists($username) {
            try {
                $PDOQuery = ('SELECT count(1) FROM userprofile_upl WHERE uid_upl = :uid');
                $PDOStatement = $this->dbh->prepare($PDOQuery);
                $PDOStatement->bindParam(':uid', $username);
                $PDOStatement->execute();
                return $PDOStatement->fetchColumn() >0;
            } catch(PDOException $e) {
                throw  new Exception('Failed to check if profile exists for '. $username, 0, $e);
            }
        }
    
        public function fetchGameUser($username) { ... }
        public function storeGameUser(GameUser $user) { ... }
    }
    

    一次性回答 Q1 和 Q2:function __construct(PDO $db) 仅表示类型约束:PHP 将检查 $db 参数值是否为 PDO 对象。如果您尝试运行$r = new GameUserRepository("not a PDO object");,它将引发错误。 PDO 类型约束与依赖注入无关。

    我认为您将此与 DI 框架的功能混淆了,该框架实际上在运行时检查构造函数签名(使用反射),看到需要 PDO 类型参数,然后确实自动创建这样一个对象并且创建存储库时将其传递给构造函数。例如。 Symfony2 DI 包可以做到这一点,但它与 PHP 本身无关。

    现在我们可以运行如下代码:

    $pdo = new PDO($connectionString, $user, $password);
    $repository = new GameUserRepository($pdo);
    $user = $repository->fetchGameUser('Dizzy');
    

    但这引出了一个问题:创建所有这些对象(服务)的最佳方法是什么,我们将它们保存在哪里?当然不是把上面的代码放在某个地方并使用全局变量。这是两个明确的职责,需要放在某个地方,所以答案是我们创建两个新类:GameContainer 类和 GameFactory 类来创建这个容器。

  • GameContainer 类的职责是将GameUserRepository 服务与我们将来创建的其他服务集中在一起。 GameFactory 类的职责是设置GameContainer 对象。我们还将创建一个GameConfig 类来配置我们的GameFactory

    class GameContainer {
        private $_gur;
        public function __construct(GameUserRepository $gur) { $this->_gur = $gur; }
        public function getUserRepository() { return $this->_gur; }
    }
    
    class GameConfig { ... }
    
    class GameFactory { 
        private $_config;
    
        public function __construct(GameConfig $cfg) {
            $this->_config = $cfg;
        }
    
        public function buildGameContainer() {
            $cfg = $this->_config;
            $pdo = new PDO($cfg->read('db.dsn'), $cfg->read('db.user'), $cfg->read('db.pw'));
            $repository = new GameUserRepository($pdo);
            return new GameContainer($repository);
        }
    }
    

我们现在可以想象一个game.php 应用程序,其基本代码如下:

$factory = new GameFactory(new GameConfig(__DIR__ . '/config.php')); 
$game = $factory->buildGameContainer();

然而,仍然缺少一个关键的东西:接口的使用。如果我们想编写一个GameUserRepository,它使用外部Web 服务来存储和获取GameUser 对象怎么办?如果我们想为MockGameUserRepository 提供带有夹具以方便测试怎么办?我们不能:我们的GameContainer 构造函数明确要求GameUserRepository 对象,因为我们使用PDO 服务实现了它。

所以,现在是重构并从我们的GameUserRepository 中提取接口的时候了。当前GameUserRepository 的所有消费者现在必须改用IGameUserRepository 接口。这要归功于依赖注入:对GameUserRepository 的引用都只是用作我们可以由接口替换的类型约束。如果我们没有将创建这些服务的任务委托给GameFactory(现在它将负责确定每个服务接口的实现),那不可能

我们现在得到这样的东西:

    interface IGameUserRepository { 
        public function profileExists($username);
        public function fetchGameUser($username);
        public function storeGameUser(GameUser $user);
    }

    class GameContainer {
        private $_gur;

        // Container only references the interface:
        public function __construct(  IGameUserRepository $gur  ) { $this->_gur = $gur; }
        public function getUserRepository() { return $this->_gur; }
    }        

    class PdoGameUserRepository implements IGameUserRepository {
        private $_db;

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

        public function profileExists($username) {...}
        public function fetchGameUser($username) { ... }
        public function storeGameUser(GameUser $user) { ... }
    }

    class MockGameUserRepository implements IGameUserRepository {

        public function profileExists($username) {
            return $username == 'Dizzy';
        }

        public function fetchGameUser($username) { 
            if ($this->profileExists($username)) {
                return new GameUser('Dizzy', 10);
            } else {
                throw new Exception("User $username does not exist.");
            }
        }

        public function storeGameUser(GameUser $user) { ... }
    }

    class GameFactory { 
        public function buildGameContainer(GameConfig $cfg) {
            $pdo = new PDO($cfg->read('db.dsn'), $cfg->read('db.user'), $cfg->read('db.pw'));

            // Factory determines which implementation to use:
            $repository = new PdoGameUserRepository($pdo);

            return new GameContainer($repository);
        }
    }

所以这真的把所有部分放在一起。我们现在可以编写一个TestGameFactory 注入MockGameUserRepository,或者更好的是,使用“env.test”布尔值扩展GameConfig,并让我们现有的GameFactory 类决定是构造一个PdoGameUserRepository 还是@987654370 @基于此。

现在与 DI 实践的联系也应该很清楚了。当然,GameContainer 是您的 DI 容器。 GameFactory,是 DI 容器工厂。正是这两个由 DI 框架(如 Symfony2 DI 包)以所有花里胡哨的方式实现。

您确实可以想象将工厂及其配置扩展到所有服务都完全定义在 XML 文件中的程度,包括它们的实现类名称:

<container env="production">
    <service name="IGameUserRepository" implementation="PdoGameUserRepository">
        <connectionString>...</connectionString>
        <username>...</username>
        <password>...</password>
    </service>
</container>

<container env="test">
    <service name="IGameUserRepository" implementation="MockGameUserRepository"/>
</container>

您还可以想象将GameContainer 泛化,以便像$container-&gt;getService('GameUserRepository') 一样获取服务。


至于在PHP中将构造函数参数传递给父类(这与DI关系不大,只是如果使用构造函数注入迟早会需要它),您可以按照自己的建议进行操作:

class A {
    private $_someService;
    public function __construct(SomeService $service) { $this->_someService = $service; }
}

class B extends class A {
    private $_someOtherService;

    public function __construct(SomeService $service, SomeOtherService $otherService) { 
        parent::__construct($service);
        $this->_someOtherService = $otherService;
    }
}

$b = new B(new SomeService(), new SomeOtherService());

但是您必须远离私有构造函数。他们散发着单身的味道。

【讨论】:

  • 感谢您抽出宝贵时间写出如此详细和解释性的答案,这让我明白了很多。我走在正确的轨道上,因为我已经为我的游戏设置了单独的类,为我的用户设置了另一个类,还有一个配置类,我使用单例作为我的数据库连接,但我想摆脱这种方法,因为我继续阅读单身人士很糟糕,你现在让工厂方法对我来说更清楚了,很遗憾我只能 +1 你。
  • 您应该得到更多 +1。感谢您的详细说明。
猜你喜欢
  • 2016-06-22
  • 2011-06-23
  • 1970-01-01
  • 1970-01-01
  • 2022-10-15
  • 2017-11-02
  • 1970-01-01
  • 1970-01-01
  • 2018-12-18
相关资源
最近更新 更多