扩展类中的PHP依赖注入(inject)

标签 php class oop dependency-injection extend

我刚刚开始处理 OOP ,很抱歉,如果这个问题似乎有点到处都是,这是我现在的感觉。

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

我有一个名为 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 时类(class)我需要注入(inject) GameUser类(class)

我认识我的新类(class) DatabaseLayerUserDatabaseLayer 继承方法和属性类(class)。

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);
        }
    }
}`

Q3:我看到有一个 parent::__construct();这是否意味着我应该使用:
private function __construct(Config $config, GameUser $user){
            parent::__construct($config);
            $this->user = $user;
        }

最佳答案

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

在我看来,如果你想引入抽象,最好从应用程序领域开始这个练习,从小处开始,然后重构。至少在学习或作为思想实验时。当更有经验并实现真实的东西时,向前看几步可能更聪明。

因此,现在我将简单地假设您正在制作一个在线游戏,其中包含由 GameUser 对象表示的不同用户,仅此而已。

  • GameUser 全权负责类应该代表域数据和逻辑,即:它包含一些与应用程序域本身(游戏用户)。它不应该负责实际的实现细节,最值得注意的是:它是如何持久化的(写入文件/数据库/任何东西)。这就是为什么 username负责存储本身可能是一个坏主意。所以让我们从一个漂亮干净的开始 score域类并使用封装(防止外部篡改的私有(private)属性):
    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;
        }
    }
    

    我们现在可以创建一个新的 incrementScore ( DatabaseLayerUser ),但我们无法坚持下去。所以我们需要某种存储库,我们可以在其中存储这些用户并在以后再次获取它们。我们就这样称呼它:GameUser .这是一个服务类。以后当有不止一种类型的域对象和存储库时,我们可以创建一个 GameUser将对这些进行分组和/或充当外观的类,但我们现在从小处着手,稍后将进行重构。
  • 再次的责任$user = new GameUser('Dizzy', 100); class 允许我们获取和存储 GameUserRepository对象,并检查给定用户名的配置文件是否存在。原则上,存储库可以存储 DatabaseLayer文件或其他地方的对象,但现在,我们在这里做出选择,将它们保存在 SQL 数据库中(从小处开始,稍后重构,你明白了。)
    GameUserRepository的责任不是管理数据库连接。但是,它需要一个数据库对象,以便将 SQL 查询传递给它。因此它委派了建立连接和实际执行它将创建的 SQL 查询的责任。

    代表团敲响了警钟。依赖注入(inject)在这里发挥作用:我们将注入(inject)一个 PDO 数据库对象(服务)。我们将它注入(inject)到构造函数中(即构造函数 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:GameUser简单地表达一个类型约束:PHP 将检查 GameUser参数值是一个 PDO 对象。如果您尝试运行 GameUserRepository ,它会抛出一个错误。 function __construct(PDO $db)类型约束与依赖注入(inject)无关。

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

    现在我们可以像这样运行代码:
    $pdo = new PDO($connectionString, $user, $password);
    $repository = new GameUserRepository($pdo);
    $user = $repository->fetchGameUser('Dizzy');
    

    但这引出了一个问题:创建所有这些对象(服务)的最佳方法是什么,我们将它们保存在哪里?当然不是将上面的代码放在某处并使用全局变量。这是两个需要在某处定位的明确职责,所以答案是我们创建两个新类:$db类和 $r = new GameUserRepository("not a PDO object");类来创建这个容器。
  • PDO的责任类是集中的GameContainer服务与我们将来创建的其他服务。 GameFactory的责任类是设置GameContainer目的。我们还将创建一个 GameUserRepository类来配置我们的 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);
        }
    }
    

  • 我们现在可以想象一个 GameContainer应用程序基本上具有以下代码:
    $factory = new GameFactory(new GameConfig(__DIR__ . '/config.php')); 
    $game = $factory->buildGameContainer();
    

    然而,仍然缺少一件关键的事情:接口(interface)的使用。如果我们想写一个 GameConfig 怎么办?使用外部 Web 服务来存储和获取 GameFactory对象?如果我们想提供一个 game.php 怎么办?用夹具方便测试?我们不能:我们的 GameUserRepository构造函数明确要求 GameUser对象,因为我们已经使用 MockGameUserRepository 实现了它服务。

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

    我们现在得到这样的东西:
        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);
            }
        }
    

    所以这真的把所有的部分放在一起。我们现在可以写一个 IGameUserRepository注入(inject) GameUserRepository ,或者更好的是,扩展 GameFactory使用“env.test” bool 值并拥有我们现有的 TestGameFactory类决定是否构造一个 MockGameUserRepositoryGameConfig基于此。

    与 DI 实践的联系现在也应该很清楚了。 GameFactory ,当然是你的DI容器。 PdoGameUserRepository ,是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>
    

    您还可以想象概括 MockGameUserRepository以便像 GameContainer 一样获取服务.

    至于将构造函数参数传递给 PHP 中的父类(这与 DI 关系不大,除非您使用构造函数注入(inject)迟早会需要它),您可以按照自己的建议执行此操作:
    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());
    

    但是您必须远离私有(private)构造函数。他们散发着单例的臭味。

    关于扩展类中的PHP依赖注入(inject),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23196263/

    相关文章:

    python - 是否建议在单个文件中包含多个类?

    c++ - 使用返回类型作为派生类的指针

    oop - 是否有可能超过OO?

    将整个目录和子目录下载到本地驱动器的 PHP FTP 库/函数

    php - IE 不会从 iframe 启动 session ?

    php - CODEIGNITER:如何获取截止日期为明天的所有行?

    php - 为什么这个 htaccess 文件在 wamp 服务器中不起作用

    c++ - 加载实例时开始 C++ 开发人员 EXC_BAD_ACCESS 错误

    php - Class::getInstance 的起源/解释?

    java - 访问修饰符在封装中有什么作用吗