我刚刚接触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' class需要有Config
类,因为我使用__construct(Config $config)
这会自动获取吗?
或者我必须同时将Config
和GameUser
传递给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);
}
}
}`
问题3:我看到有一个parent::__construct();
这是否意味着我应该使用:
private function __construct(Config $config, GameUser $user){
parent::__construct($config);
$this->user = $user;
}
答案 0 :(得分:18)
我认为你真的在这里混淆了手段和目的。目标永远不是使用依赖注入本身,而是解决您遇到的编程问题。因此,让我们首先尝试并稍微纠正类之间的依赖关系。
在我看来,最好从应用程序域开始这个练习,从小开始,然后重构,如果你想引入抽象。至少在学习或作为思想实验时。如果经验更丰富,实施真实的东西,那么前面几步可能会更聪明。
所以现在我只是假设您正在制作一个在线游戏,其中不同的用户由GameUser对象代表,并且不会超过这个。
GameUser
类的唯一责任应该是表示域数据和逻辑,即:它包含几个属性(让我们说username
和{{ 1}})和方法(让我们说score
)与应用程序域本身(游戏用户)有关。它应该不负责实际的实现细节,最值得注意的是:它如何被持久化(写入文件/ db /等)。这就是为什么负责存储自身的incrementScore
可能是一个坏主意。因此,让我们从一个漂亮而干净的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
对象存储在文件或其他地方,但是现在,我们在这里做出选择以将它们保存在SQL数据库中(从小开始,稍后重构,你得到它。)
GameUser
的责任是不管理数据库连接。但是它需要一个数据库对象,因此它可以将SQL查询传递给它。因此,它委托设置连接并实际执行它将创建的SQL查询的责任。
代表团敲响了钟声。依赖注入在这里发挥作用:我们将注入一个PDO数据库对象(服务)。我们将它注入构造函数(即构造函数DI而不是setter DI)。然后,调用者有责任弄清楚如何创建PDO服务,我们的存储库真的不在乎。
GameUserRepository
回答Q1& Q2中的一个: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) { ... }
}
只表示一个类型约束:PHP将检查function __construct(PDO $db)
参数值是否为PDO对象。如果您尝试运行$db
,则会产生错误。 $r = new GameUserRepository("not a PDO object");
类型约束与依赖注入无关。
我认为你将这与在运行时实际检查构造函数签名的DI框架的功能混淆(使用反射),看到需要PDO类型的参数,然后确实创建了这样的对象自动创建存储库时将其传递给构造函数。例如。 Symfony2 DI包可以做到这一点,但它与PHP本身无关。
现在我们可以运行这样的代码:
PDO
但这引出了一个问题:创建所有这些对象(服务)的最佳方法是什么,我们在哪里保留它们?当然不是仅仅将上面的代码放在某处并使用全局变量。这些是需要在某处定位的两个明确的职责,所以答案是我们创建了两个新类:$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
然而,仍然缺少一个关键的事情:接口的使用。如果我们要编写使用外部Web服务来存储和获取$factory = new GameFactory(new GameConfig(__DIR__ . '/config.php'));
$game = $factory->buildGameContainer();
个对象的GameUserRepository
,该怎么办?如果我们想提供一个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
,或者更好的是,使用" env.test"扩展MockGameUserRepository
。 boolean并让我们现有的GameConfig
类根据它来决定是构建GameFactory
还是PdoGameUserRepository
。
现在也应该清楚与DI实践的联系。当然,MockGameUserRepository
是您的DI容器。 GameContainer
是DI容器工厂。这两个是通过DI框架(如Symfony2 DI捆绑包)实现所有铃声和口哨声。
您确实可以想象将工厂及其配置扩展到所有服务在XML文件中完全定义的程度,包括它们的实现类名称:
GameFactory
您还可以设想概括<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
一样提取服务。
至于将构造函数参数传递给PHP中的父类(与DI几乎没有关系,除非你迟早需要使用构造函数注入),你可以按照自己的建议做到这一点:
$container->getService('GameUserRepository')
但是你必须远离私人建筑师。他们对单身人士充满了热情。