注意:虽然直接回答ops 问题,“我什么时候只能在需要时而不是在每个请求时创建/连接到数据库”是在您需要时注入它,只是说这没有帮助。我在这里解释你实际上是如何正确地做到这一点的,因为在非特定框架上下文中确实没有很多有用的信息可以在这方面提供帮助。
更新:这个问题的“旧”答案可以在下面看到。这鼓励了服务定位器模式,这种模式非常有争议,对许多人来说是“反模式”。新的答案加上我从研究中学到的东西。请先阅读旧答案,看看进展如何。
新答案
在使用 pimple 一段时间后,我了解了很多关于它的工作原理,以及它实际上并没有那么神奇。它仍然很酷,但它只有 80 行代码的原因是它基本上允许创建闭包数组。Pimple 经常用作服务定位器(因为它的实际功能非常有限),这是一种“反模式”。
首先,什么是服务定位器?
服务定位器模式是一种用于软件开发的设计模式,用于封装获得具有强大抽象层的服务所涉及的过程。此模式使用称为“服务定位器”的中央注册表,它根据请求返回执行特定任务所需的信息。
我在引导程序中创建 pimple,定义依赖项,然后将此容器传递给我实例化的每个类。
为什么服务定位器不好?
你说这个有什么问题?主要问题是这种方法隐藏了类的依赖关系。因此,如果开发人员要更新这个类并且他们以前没有见过它,他们将看到一个包含未知数量对象的容器对象。此外,测试这个类将是一场噩梦。
我最初为什么要这样做?因为我认为在控制器之后是你开始进行依赖注入的地方。这是错误的。您可以直接在控制器级别启动它。
如果这是我的应用程序中的工作方式:
Front Controller --> Bootstrap --> Router --> Controller/Method --> Model [Services|Domain Objects|Mappers] --> Controller --> View --> Template
...然后依赖注入容器应该立即在第一个控制器级别开始工作。
所以真的,如果我仍然使用 pimple,我将定义将要创建的控制器以及它们需要什么。因此,您可以将视图和模型层中的任何内容注入控制器,以便它可以使用它。这是控制反转,使测试更容易。来自 Aurn wiki,(我很快就会谈到):
在现实生活中,您不会通过将整个五金店(希望如此)运送到建筑工地来建造房屋,这样您就可以访问您需要的任何零件。相反,工头 (__construct()) 询问将需要的特定部件(门和窗)并着手采购它们。您的对象应该以相同的方式运行;他们应该只询问完成工作所需的特定依赖项。让 House 访问整个硬件商店充其量是糟糕的 OOP 风格,最坏的情况是可维护性的噩梦。- 来自 Auryn 维基
进入奥林
在这方面,我想向您介绍一个名为Auryn的出色的东西,它是由Rdlowrey写的,我是在周末被介绍给我的。
Auryn 'auto-wires' 基于类构造函数签名的类依赖项。这意味着,对于每个请求的类,Auryn 都会找到它,在构造函数中找出它需要什么,首先创建它需要的东西,然后创建你最初请求的类的实例。以下是它的工作原理:
Provider 根据构造函数方法签名中指定的参数类型提示递归地实例化类依赖项。
...如果您对PHP 的反射有所了解,您就会知道有些人称它为“慢”。所以这就是 Auryn 的做法:
您可能听说过“反射很慢”。让我们澄清一下:如果你做错了,任何事情都可能“太慢”。反射比磁盘访问快一个数量级,比从远程数据库检索信息(例如)快几个数量级。此外,如果您担心速度,每个反射都提供了缓存结果的机会。Auryn 缓存它生成的任何反射,以最大限度地减少潜在的性能影响。
所以现在我们已经跳过了“反射很慢”的论点,这就是我一直在使用它的方式。
我如何使用 Auryn
我让 Auryn 成为我的 autoloader 的一部分。这样当一个类被请求时,Auryn 可以离开并读取该类及其依赖项,以及依赖项的依赖项(等等),并将它们全部返回到类中以进行实例化。我创建了 Auyrn 对象。
$injector = new \Auryn\Provider(new \Auryn\ReflectionPool);
我在数据库类的构造函数中使用数据库接口作为要求。所以我告诉 Auryn 使用哪个具体的实现(如果你想在代码中的一个点实例化不同类型的数据库,这是你改变的部分,它仍然可以工作)。
$injector->alias('Library\Database\DatabaseInterface', 'Library\Database\MySQL');
如果我想更改为 MongoDB 并为它编写了一个类,我会简单地更改Library\Database\MySQL
为Library\Database\MongoDB
.
然后,我将 传递给$injector
我的路由器,并在创建控制器/方法时,这是自动解决依赖关系的地方。
public function dispatch($injector)
{
// Make sure file / controller exists
// Make sure method called exists
// etc...
// Create the controller with it's required dependencies
$class = $injector->make($controller);
// Call the method (action) in the controller
$class->$action();
}
最后,回答OP的问题
好的,所以使用这种技术,假设您有一个用户控制器,它需要用户服务(比如用户模型),它需要数据库访问。
class UserController
{
protected $userModel;
public function __construct(Model\UserModel $userModel)
{
$this->userModel = $userModel;
}
}
class UserModel
{
protected $db;
public function __construct(Library\DatabaseInterface $db)
{
$this->db = $db;
}
}
如果您使用路由器中的代码,Auryn 将执行以下操作:
- 创建 Library\DatabaseInterface,使用 MySQL 作为具体类(在 boostrap 中使用别名)
- 使用之前创建的数据库创建“用户模型”注入其中
- 创建 UserController 并将之前创建的 UserModel 注入其中
这就是递归,这就是我之前所说的“自动布线”。这解决了 OPs 问题,因为只有当类层次结构包含数据库对象作为构造函数要求时,才会实例化对象,而不是在每个请求时。
此外,每个类都有它们在构造函数中运行所需的确切要求,因此没有像服务定位器模式那样隐藏的依赖关系。
RE:如何制作以便在需要时调用连接方法。这真的很简单。
- 确保在数据库类的构造函数中,不要实例化对象,只需传入它的设置(主机、数据库名、用户、密码)。
new PDO()
有一个使用类的设置实际执行对象的连接方法。
class MySQL implements DatabaseInterface
{
private $host;
// ...
public function __construct($host, $db, $user, $pass)
{
$this->host = $host;
// etc
}
public function connect()
{
// Return new PDO object with $this->host, $this->db etc
}
}
因此,现在,您将数据库传递给的每个类都将具有此对象,但还没有连接,因为尚未调用 connect()。
- 在可以访问 Database 类的相关模型中,您调用
$this->db->connect();
然后继续您想做的事情。
本质上,您仍然使用我之前描述的方法将您的数据库对象传递给需要它的类,但是要决定何时在逐个方法的基础上执行连接,您只需运行所需的 connect 方法一。不,您不需要单身人士。您只需在需要时告诉它何时连接,当您不告诉它连接时它不会。
旧答案
我将更深入地解释依赖注入容器,以及它们如何帮助您解决问题。注意:理解“MVC”的原理在这里会有很大帮助。
问题
您想创建一些对象,但只有某些对象需要访问数据库。您目前正在做的是在每个请求上创建数据库对象,这完全没有必要,而且在使用 DiC 容器之类的东西之前也是完全常见的。
两个示例对象
这是您可能要创建的两个对象的示例。一个需要数据库访问,另一个不需要数据库访问。
/**
* @note: This class requires database access
*/
class User
{
private $database;
// Note you require the *interface* here, so that the database type
// can be switched in the container and this will still work :)
public function __construct(DatabaseInterface $database)
{
$this->database = $database;
}
}
/**
* @note This class doesn't require database access
*/
class Logger
{
// It doesn't matter what this one does, it just doesn't need DB access
public function __construct() { }
}
那么,创建这些对象并处理它们的相关依赖关系以及仅将数据库对象传递给相关类的最佳方法是什么?好吧,对我们来说幸运的是,当使用依赖注入容器时,这两者可以和谐地协同工作。
进入疙瘩
Pimple是一个非常酷的依赖注入容器(由 Symfony2 框架的制造商提供),它利用了PHP 5.3+ 的闭包。
pimple 的做法真的很酷——你想要的对象在你直接请求之前不会被实例化。因此,您可以设置大量新对象,但在您请求它们之前,它们不会被创建!
这是您在boostrap中创建的一个非常简单的疙瘩示例:
// Create the container
$container = new Pimple();
// Create the database - note this isn't *actually* created until you call for it
$container['datastore'] = function() {
return new Database('host','db','user','pass');
};
然后,在此处添加 User 对象和 Logger 对象。
// Create user object with database requirement
// See how we're passing on the container, so we can use $container['datastore']?
$container['User'] = function($container) {
return new User($container['datastore']);
};
// And your logger that doesn't need anything
$container['Logger'] = function() {
return new Logger();
};
惊人的!那么..我如何实际使用 $container 对象?
好问题!因此,您已经在引导程序中$container
创建了对象并设置了对象及其所需的依赖项。在您的路由机制中,您将容器传递给您的控制器。
注意:示例基本代码
router->route('controller', 'method', $container);
在您的控制器中,您访问$container
传入的参数,当您从它请求用户对象时,您会返回一个新的用户对象(工厂样式),其中数据库对象已经注入!
class HomeController extends Controller
{
/**
* I'm guessing 'index' is your default action called
*
* @route /home/index
* @note Dependant on .htaccess / routing mechanism
*/
public function index($container)
{
// So, I want a new User object with database access
$user = $container['User'];
// Say whaaat?! That's it? .. Yep. That's it.
}
}
你解决了什么
所以,你现在已经用一块石头杀死了多只鸟(不仅仅是两只)。
- 在每个请求上创建一个 DB 对象- 不再是!它仅在您请求时创建,因为 Pimple 使用了闭包
- 从控制器中删除“新”关键字- 是的,没错。您已将此责任移交给容器。
注意:在继续之前,我想指出第二点的重要性。如果没有这个容器,假设您在整个应用程序中创建了 50 个用户对象。然后有一天,您想添加一个新参数。OMG - 您现在需要检查整个应用程序并将此参数添加到每个new User()
. 但是,使用 DiC - 如果您在$container['user']
任何地方使用,您只需将第三个参数添加到容器中一次,仅此而已。是的,这太棒了。
- 切换数据库的能力——你听我说,重点是如果你想从 MySQL 更改为 PostgreSQL——你更改容器中的代码以返回你编码的新的不同类型的数据库,并且作为只要它都返回相同的东西,就是这样!交换每个人都在谈论的具体实现的能力。
重要的部分
这是使用容器的一种方式,这只是一个开始。有很多方法可以使它变得更好 - 例如,您可以使用反射/某种映射来决定需要容器的哪些部分,而不是将容器交给每个方法。自动化这个,你是金子。
我希望你觉得这很有用。我在这里完成的方式至少为我节省了大量的开发时间,而且启动起来很有趣!