扩展类中的PHP依赖注入

Diz*_*igh 3 php oop dependency-injection class extend

我只是想要掌握OOP,很抱歉,如果这个问题似乎有点遍布整个地方,那就是我现在的感觉.

我已经在PHP文档中查看了构造函数,但它似乎没有涵盖依赖注入.

我有一个名为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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Q1:我有private function __construct(Config $config) 我不知道我完全理解其原因__construct(Config $config),而不是仅仅使用__construct($config) 不(Config $config)自动创建$config一个新的Config实例?

或者我必须做以下事情:

$config = new Config();
$dbLayer = new DatabaseLayer($config);
Run Code Online (Sandbox Code Playgroud)

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

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

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

还是我必须要同时通过ConfigGameUserDatabaseLayerUser

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);
        }
    }
}`
Run Code Online (Sandbox Code Playgroud)

Q3:我看到有一个parent::__construct(); 意思是我应该使用:

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

wka*_*ann 19

我认为你真的在这里混淆了手段和目的.目标永远不是使用依赖注入本身,而是解决您遇到的编程问题.因此,让我们首先尝试并更正类和它们的职责之间的依赖关系.

在我看来,最好从应用程序域开始这个练习,从小开始,然后重构,如果你想引入抽象.至少在学习或作为思想实验时.如果更有经验并且在实施真实的东西时,可能会更聪明地看一下前面的几个步骤.

所以现在我只是假设你正在制作一个在线游戏,其中不同的用户由GameUser对象代表,并且没有更多.

  • GameUser该类的唯一责任应该是表示域数据和逻辑,即:它包含与应用程序域本身(游戏用户)有关的几个属性(比如说usernamescore)和方法(比方说incrementScore).它应该对实际的实现细节负责,最明显的是:它如何被持久化(写入文件/ db /无论如何).这就是为什么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;
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    我们现在可以创建一个new 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) { ... }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    要一次性回答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');
    
    Run Code Online (Sandbox Code Playgroud)

    但这引出了一个问题:创建所有这些对象(服务)的最佳方法是什么,我们在哪里保留它们?当然不是仅仅将上面的代码放在某处并使用全局变量.这些是需要在某处定位的两个明确的职责,所以答案是我们创建了两个新类:创建此容器的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);
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

我们现在可以想象一个game.php基本上有以下代码的应用程序:

$factory = new GameFactory(new GameConfig(__DIR__ . '/config.php')); 
$game = $factory->buildGameContainer();
Run Code Online (Sandbox Code Playgroud)

然而,一个关键的问题仍然是缺失:接口的使用.如果我们想编写一个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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

现在,与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>
Run Code Online (Sandbox Code Playgroud)

你也可以设想一般化,GameContainer以便提取服务$container->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());
Run Code Online (Sandbox Code Playgroud)

但你必须远离私人建筑师.他们对单身人士充满了热情.