如何构建PHP依赖注入容器
如何构建PHP依赖注入容器
我最近学习了在PHP应用程序中使用依赖注入(Dependency Injection, DI)的优点。然而,我仍然不确定如何创建我的依赖项容器,或者我是否应该在我正在构建的在线论坛中使用DI。
以下的代码是我基于从这里学习的示例编写的DI容器的版本。
class ioc { var $db; var $session; var $user_id; static function newUser(static::$db, static::$user_id) { $user = new User($db, $user_id); return $user; } static function newLogin(static::$db, static::$session) { $login = new Login($db, $session); return $login; } } $user = ioc::newUser(); $login = ioc::newLogin();
我有几个问题:
1)我应该在哪里实例化我的注入依赖项,比如$database、$session等?应该在容器类外还是在容器的构造函数内?
2)如果我需要在其他类中创建多个User类的实例怎么办?我不能注入先前实例化的$user对象,因为该实例已被使用。然而,在另一个类中创建多个User实例会违反DI的规则。例如:
class Users { function __construct($db, $user_id) { $this->db = $db; $this->user_id = $user_id; } function create_friends_list() { $st = $this->$db->prepare("SELECT user_id FROM friends WHERE user_id = $this->user_id"); $st->execute(); while($row = $st->fetch()) { $friend = ioc::newUser($row['user_id']); $friend->get_user_name(); $friend->get_profile_picture(); } } }
3)我正在思考是否应该采用DI,知道我必须重写我以前的所有代码。我先前依赖于我在initialize.php中实例化的全局变量,该文件被包含在所有我的文件中。
在我看来,DI会带来很多开销,并且有些情况下会无法使用(例如第2个问题)。以下网站是一个开发人员列举不使用DI的很多好理由。他的论点有任何价值吗?还是我只是错误地使用了DI?
我本想将此作为评论撰写,但它变得太长了。我不是专家,因此我只会从我在实践中学到的东西和我的一些看法来回答。请随意使用或者质疑我的回答中的任何部分(或者都不用)。
1. 外部容器的功能是什么?答案应该是单一的。它不需要负责初始化类、连接数据库和管理会话等各种事情。每个类只做单一的事情。
class ioc { public $db; // Only pass here the things that the class REALLY needs static public function set($var, $val) { return $this->$var = $val; } static function newDB($user, $pass) { return new PDO('mysql:host=localhost;dbname=test', $user, $pass); } static function newUser($user_id) { return new User($db, $user_id); } static function newLogin($session) { return new Login($this->db, $session); } } if (ioc::set('db',ioc::newDB($user, $pass))) { $user = ioc::newUser($user_id); $login = ioc::newLogin($session); }
2. 内部的类不应该执行 $friend = ioc::newUser($row['user_id']);
这样的操作。因为此时你就假定存在名为 ioc 的类并且其拥有名为 newUser() 的方法了,但每个类都应该能够独立操作,不能基于[可能]存在的其他类。这被称为紧耦合。基本上,这就是为什么你不应该使用全局变量的原因之一。类内需要使用的任何内容都应该传递过去,而不是在全局范围内假定。即使你知道全局变量的存在,你的代码也能够运行,但这会使得类无法重用于其他项目并且更难进行测试。我不会过于拓展这个话题,但是我会分享一个在 SO 上发现的很棒的视频:代码整洁之道 - 不要寻找事物。
我不确定类 User
的行为如何,但这是我会做的事情(不一定是正确的):
// Allow me to change the name to Friends to avoid confusions class Friends { function __construct($db) { $this->db = $db; } function create_friends_list($user_id) { if (!empty(id)) { // Protect it from injection if your $user_id MIGHT come from a $_POST or whatever $st = $this->$db->prepare("SELECT user_id FROM friends WHERE user_id = ?"); $st->execute(array($user_id)); $AllData = $st->fetchAll() return $AllData; } else return null; } // Pass the $friend object function get_friend_data($friend) { $FriendData = array ('Name' => $friend->get_user_name(), 'Picture' => $friend->get_profile_picture()); return $FriendData; } } $User = ioc::newUser($user_id); $Friends = new Friends($db); $AllFriendsIDs = array(); if ($AllFriendsIDs = $Friends->create_friends_list($User->get('user_id'))) foreach ($AllFriendsIDs as $Friend) { // OPTION 1. Return the name, id and whatever in an array for the user object passed. $FriendData = $Friends->get_friend_data(ioc::newUser($Friend['user_id'])); // Do anything you want with $FriendData // OPTION 2. Ditch the get_friend_data and work with it here directly. You're already in a loop. // Create the object (User) Friend. $Friend = ioc::newUser($Friend['user_id']); $Friend->get_user_name(); $Friend->get_profile_picture(); }
我没有测试它,所以它可能会有些小的错误。
3. 如果你是在编码中学习的话,则你将需要重写许多东西。尝试从一开始就做对某些事情,这样你就不需要重写所有的东西,只需要重写几个类或者方法,然后采用一些适用于你所有代码的约定。例如,永远不要从函数/方法内部使用 echo,而是始终从外部返回和输出 echo。我敢说是值得这样做的。虽然不好的是你可能不得不花费一个或两个小时来重写某些东西,但如果必须这样做,那就去完成它吧。
顺便说一句,抱歉,我已经在所有地方改变了你的括号风格。
编辑
阅读其他答案时,虽然你不应该使用 ioc 对象连接数据库,但是使用它来创建一个新对象是完全可以的。上文已对此进行了修改,以便您了解我所指的含义。
我的注入依赖项 (例如 $database、$session 等) 应该在哪里实例化? 是在容器类外面还是在容器构造函数内部?
理想情况下,你的数据库连接和会话会被引导进来。正确的 DI 要求一个基础对象实例,所有东西都在其中注册。以您的 IOC 类为例,需要创建一个它的实例 ($ioc = new IOC();
),然后需要某种服务提供者类,比如:
$ioc->register('database', new DatabaseServiceProvider($host, $user, $pass))
现在,每次想要连接到数据库时,只需要传递 $ioc->get('database');
。这只是一个非常粗略的例子,但我认为你可以看到基本思想是将所有内容存储在注册表内,并且没有任何静态绑定,这意味着您可以创建另一个实例的 $ioc
,拥有完全不同的设置,使得轻松创建到不同的数据库的连接,以进行测试。
如果我需要在其他类中创建 User 类的多个实例该怎么办? 我不能注入已经实例化的 $user 对象,因为该实例已经被使用。但是,在另一个类中创建多个 User 实例会违反 DI 的规则。
这是一个常见问题,有多种不同的解决方案。首先,您的 DI 应该显示已登录用户和普通用户之间的差异。您可能想要注册已登录的用户,但不是所有用户。使您的用户类正常化,并使用:
$ioc->register('login-user', User::fetch($ioc->get('database'), $user_id));
现在,$ioc->get('login-user')
返回您已登录的用户。然后,您可以使用 User->fetchAll($ioc->get('database'));
来获取您的所有用户。
我在思考是否应该采用 DI,知道我必须重写我所有的之前的代码。我之前依赖全局变量,我在 initialize.php 中实例化它,在我所有的文件中包括它。
如果您需要重写所有代码来使用 DI,则可能不应该这样做。也许可以考虑创建一个新项目,并在其中引用一些旧代码(如果你有时间的话)。如果您的代码库很大,我建议将其拆分成较小的项目,并使用 RESTFUL API 获取和保存数据。将您的论坛搜索放入单独的应用中是编写 API 的很好的例子:/search/name?partial-name=bob 将返回所有带有该单词的用户。您可以逐步构建它并使其更好,并在主论坛中使用它。
我希望你能理解我的回答,如果需要更多信息,请让我知道。