依赖注入与服务定位

最后更新于:2022-04-02 05:13:51

[TOC] # 依赖注入与服务定位 ## DI 说明 以下示例有点长,但它试图解释为什么Phalcon使用服务定位器和依赖注入。首先,我们假设我们正在开发一个名为`SomeComponent`的组件。这执行一些任务。我们的组件具有依赖关系,即与数据库的连接。 在第一个示例中,在组件内部创建连接。虽然这是一个非常有效的实现,但它是公正的,因为我们无法更改连接参数或数据库系统的类型,因为组件只能按创建的方式工作。 ```php 'localhost', 'username' => 'root', 'password' => 'secret', 'dbname' => 'invo', ] ); // ... } } $some = new SomeComponent(); $some->someDbTask(); ``` 为了解决这个缺点,我们创建了一个在使用它之前在外部注入依赖项的setter。这也是一个有效的实现,但有其缺点: ```php connection = $connection; } public function someDbTask() { $connection = $this->connection; // ... } } $some = new SomeComponent(); // 创建连接 $connection = new Connection( [ 'host' => 'localhost', 'username' => 'root', 'password' => 'secret', 'dbname' => 'invo', ] ); // 注入组件中的连接 $some->setConnection($connection); $some->someDbTask(); ``` 现在考虑我们在应用程序的不同部分使用此组件,然后在将其传递给组件之前需要多次创建连接。使用全局注册表模式,我们可以在那里存储连接对象,并在需要时重用它。 ```php 'localhost', 'username' => 'root', 'password' => 'secret', 'dbname' => 'invo', ] ); } } class SomeComponent { protected $connection; /** * 在外部设置连接 * * @param Connection $connection */ public function setConnection(Connection $connection) { $this->connection = $connection; } public function someDbTask() { $connection = $this->connection; // ... } } $some = new SomeComponent(); // 传递注册表中定义的连接 $some->setConnection(Registry::getConnection()); $some->someDbTask(); ``` 现在,让我们假设我们必须在组件中实现两个方法,第一个总是需要创建一个新连接,第二个总是需要使用共享连接: ```php 'localhost', 'username' => 'root', 'password' => 'secret', 'dbname' => 'invo', ] ); } /** * 仅创建一次连接并返回它 * * @return Connection */ public static function getSharedConnection(): Connection { if (self::$connection === null) { self::$connection = self::createConnection(); } return self::$connection; } /** * 始终返回新连接 * * @return Connection */ public static function getNewConnection(): Connection { return self::createConnection(); } } class SomeComponent { protected $connection; /** * 在外部设置连接 * * @param Connection $connection */ public function setConnection(Connection $connection) { $this->connection = $connection; } /** * 此方法始终需要共享连接 */ public function someDbTask() { $connection = $this->connection; // ... } /** * 此方法始终需要新连接 * * @param Connection $connection */ public function someOtherDbTask(Connection $connection) { } } $some = new SomeComponent(); // 这会注入共享连接 $some->setConnection( Registry::getSharedConnection() ); $some->someDbTask(); // 在这里,我们总是传递一个新的连接作为参数 $some->someOtherDbTask( Registry::getNewConnection() ); ``` 到目前为止,我们已经看到依赖注入如何解决我们的问将依赖项作为参数传递而不是在代码内部创建它们使我们的应用程序更易于维护和解耦。然而,从长远来看,这种形式的依赖注入有一些缺点。 例如,如果组件有很多依赖项,我们需要创建多个setter参数来传递依赖项或创建一个构造函数,用多个参数传递它们,另外在使用组件之前创建依赖项,使我们的代码不像我们希望的那样可维护: ```php setConnection($connection); $some->setSession($session); $some->setFileSystem($fileSystem); $some->setFilter($filter); $some->setSelector($selector); ``` 想想我们是否必须在应用程序的许多部分中创建此对象。将来,如果我们不需要任何依赖项,我们需要遍历整个代码库,以在我们注入代码的任何构造函数或setter中删除参数。为了解决这个问题,我们再次返回全局注册表来创建组件。但是,它在创建对象之前添加了一个新的抽象层: ```php di = $di; } public function someDbTask() { // 获取连接服务 // 始终返回新连接 $connection = $this->di->get('db'); } public function someOtherDbTask() { // 获取共享连接服务, // 这将每次返回相同的连接 $connection = $this->di->getShared('db'); // 此方法还需要输入过滤服务 $filter = $this->di->get('filter'); } } $di = new Di(); // 在容器中注册“db”服务 $di->set( 'db', function () { return new Connection( [ 'host' => 'localhost', 'username' => 'root', 'password' => 'secret', 'dbname' => 'invo', ] ); } ); // 在容器中注册“过滤器”服务 $di->set( 'filter', function () { return new Filter(); } ); // 在容器中注册“session”服务 $di->set( 'session', function () { return new Session(); } ); // 将服务容器作为唯一参数传递 $some = new SomeComponent($di); $some->someDbTask(); ``` 现在,组件可以在需要时简单地访问所需的服务,如果它不需要服务,甚至不会初始化,从而节省资源。该组件现在高度分离。例如,我们可以替换创建连接的方式,它们的行为或它们的任何其他方面以及不会影响组件的方式。 `Phalcon\Di`是实现依赖注入和服务定位的组件,它本身就是它们的容器。 由于Phalcon高度解耦,`Phalcon\Di`对于整合框架的不同组件至关重要。开发人员还可以使用此组件注入依赖项并管理应用程序中使用的不同类的全局实例。 基本上,该组件实现了控制反转模式。应用此方法,对象不会使用setter或构造函数接收它们的依赖项,而是请求服务依赖项注入器。这降低了整体复杂性,因为只有一种方法可以在组件中获得所需的依赖关系。 此外,这种模式增加了代码的可测试性,从而使其不易出错。 ## 在容器中注册服务 框架本身或开发人员可以注册服务。当组件A需要组件B(或其类的实例)进行操作时,它可以从容器请求组件B,而不是创建新的实例组件B. 这种工作方式给我们带来了许多好处: * 我们可以轻松地用自己或第三方创建的组件替换组件。 * 我们完全控制对象初始化,允许我们在将它们传递给组件之前根据需要设置这些对象。 * 我们可以以结构化和统一的方式获取组件的全局实例。 可以使用几种类型的定义注册服务: ### 简单注册 如前所述,有几种方法可以注册服务。我们称之为简单注册: #### 字符串 此类型需要有效类的名称,返回指定类的对象,如果未加载该类,则将使用自动加载器对其进行实例化。这种类型的定义不允许为类构造函数或参数指定参数: ```php set( 'request', 'Phalcon\Http\Request' ); ``` #### 类实例 此类型需要一个对象。由于对象不需要被解析,因为它已经是一个对象,可以说它实际上不是一个依赖注入,但是,如果要强制返回的依赖项始终是相同的对象/值,则它很有用: ```php set( 'request', new Request() ); ``` #### 闭包/匿名函数 此方法提供了更大的自由度来构建依赖项,但是,很难在不必完全更改依赖项定义的情况下从外部更改某些参数: ```php set( 'db', function () { return new PdoMysql( [ 'host' => 'localhost', 'username' => 'root', 'password' => 'secret', 'dbname' => 'blog', ] ); } ); ``` 通过将其他变量传递给闭包的环境可以克服一些限制: ```php '127.0.0.1', 'username' => 'user', 'password' => 'pass', 'dbname' => 'my_database', ] ); // 在当前作用域中使用 $config 变量 $di->set( 'db', function () use ($config) { return new PdoMysql( [ 'host' => $config->host, 'username' => $config->username, 'password' => $config->password, 'dbname' => $config->name, ] ); } ); ``` 您还可以使用`get()`方法访问其他DI服务: ```php set( 'config', function () { return new Config( [ 'host' => '127.0.0.1', 'username' => 'user', 'password' => 'pass', 'dbname' => 'my_database', ] ); } ); // 使用DI中的'config'服务 $di->set( 'db', function () { $config = $this->get('config'); return new PdoMysql( [ 'host' => $config->host, 'username' => $config->username, 'password' => $config->password, 'dbname' => $config->name, ] ); } ); ``` ### 复杂注册 如果需要在不实例化/解析服务的情况下更改服务定义,那么我们需要使用数组语法定义服务。使用数组定义定义服务可能会更冗长: ```php set( 'logger', [ 'className' => 'Phalcon\Logger\Adapter\File', 'arguments' => [ [ 'type' => 'parameter', 'value' => '../apps/logs/error.log', ] ] ] ); // 使用匿名函数 $di->set( 'logger', function () { return new LoggerFile('../apps/logs/error.log'); } ); ``` 上述两个服务注册都会产生相同的结果。但是,数组定义允许在需要时更改服务参数: ```php getService('logger') ->setClassName('MyCustomLogger'); // 更改第一个参数而不实例化记录器 $di ->getService('logger') ->setParameter( 0, [ 'type' => 'parameter', 'value' => '../apps/logs/error.log', ] ); ``` 此外,通过使用数组语法,您可以使用三种类型的依赖注入: #### 构造函数注入 此注入类型将依赖项/参数传递给类构造函数。让我们假装我们有以下组件: ```php response = $response; $this->someFlag = $someFlag; } } ``` 该服务可以这种方式注册: ```php set( 'response', [ 'className' => 'Phalcon\Http\Response' ] ); $di->set( 'someComponent', [ 'className' => 'SomeApp\SomeComponent', 'arguments' => [ [ 'type' => 'service', 'name' => 'response', ], [ 'type' => 'parameter', 'value' => true, ], ] ] ); ``` 服务'response'(`Phalcon\Http\Response`)被解析为作为构造函数的第一个参数传递,而第二个是一个布尔值(true),它按原样传递。 #### Setter 注入 类可能有setter来注入可选的依赖项,我们以前的类可以更改为接受setter的依赖项: ```php response = $response; } public function setFlag($someFlag) { $this->someFlag = $someFlag; } } ``` 具有setter注入的服务可以注册如下: ```php set( 'response', [ 'className' => 'Phalcon\Http\Response', ] ); $di->set( 'someComponent', [ 'className' => 'SomeApp\SomeComponent', 'calls' => [ [ 'method' => 'setResponse', 'arguments' => [ [ 'type' => 'service', 'name' => 'response', ] ] ], [ 'method' => 'setFlag', 'arguments' => [ [ 'type' => 'parameter', 'value' => true, ] ] ] ] ] ); ``` #### 属性注入 一种不太常见的策略是将依赖项或参数直接注入到类的公共属性中: ```php set( 'response', [ 'className' => 'Phalcon\Http\Response', ] ); $di->set( 'someComponent', [ 'className' => 'SomeApp\SomeComponent', 'properties' => [ [ 'name' => 'response', 'value' => [ 'type' => 'service', 'name' => 'response', ], ], [ 'name' => 'someFlag', 'value' => [ 'type' => 'parameter', 'value' => true, ], ] ] ] ); ``` 支持的参数类型包括以下内容: | Type | 描述 | 例子 | | :-------: | :------------------------: | :----------------------------------------------------------: | | parameter | 表示要作为参数传递的文本值 | `php['type' => 'parameter', 'value' => 1234]` | | service | 表示服务容器中的另一个服务 | `php['type' => 'service', 'name' => 'request']` | | instance | 表示必须动态构建的对象 | `php['type' => 'instance', 'className' => 'DateTime', 'arguments' => ['now']]` | 解析定义复杂的服务可能比先前看到的简单定义稍慢。但是,这些提供了一种更健壮的方法来定义和注入服务。允许混合使用不同类型的定义,每个人都可以根据应用程序需求决定注册服务的最合适方式。 ### Array Syntax 数组也允许注册服务: ```php 'Phalcon\Http\Request', ]; ``` 在上面的示例中,当框架需要访问请求数据时,它将要求在容器中标识为“请求”的服务。容器反过来将返回所需服务的实例。开发人员可能最终在他/她需要时替换组件。 用于设置/注册服务的每种方法(在以上示例中说明)具有优点和缺点。由开发人员和将指定使用哪一个的特定要求决定。 通过字符串设置服务很简单,但缺乏灵活性。使用数组设置服务提供了更大的灵活性,但使代码更复杂。lambda函数在两者之间是一个很好的平衡,但可能导致比预期更多的维护。 `Phalcon\Di` 为其存储的每项服务提供延迟加载。除非开发人员选择直接实例化对象并将其存储在容器中,否则存储在其中的任何对象(通过数组,字符串等)将被延迟加载,即仅在请求时进行实例化。 ### 从YAML文件加载服务 此功能将允许您在`yaml`文件中或仅在纯PHP中设置服务。例如,您可以使用`yaml`文件加载服务,如下所示: ```yaml config: className: \Phalcon\Config shared: true ``` ```php loadFromYaml('services.yml'); $di->get('config'); // will properly return config service ``` >[danger] 此方法要求安装模块Yaml。 有关详细信息,请参阅[this](http://php.net/manual/book.yaml.php)。 ## 解析服务 从容器中获取服务只需调用“get”方法即可。将返回该服务的新实例: ```php $request = $di->get('request'); ``` 或者通过魔术方法调用: ```php $request = $di->getRequest(); ``` 或者使用数组访问: ```php $request = $di['request']; ``` 通过向方法'get'添加数组参数,可以将参数传递给构造函数: ```php get( 'MyComponent', [ 'some-parameter', 'other', ] ); ``` ### Events `Phalcon\Di` 能够将事件发送到:`EventsManager `(如果存在)。使用“di”类型触发事件。返回布尔值false时的某些事件可能会停止活动操作。支持以下事件: | Event Name | Triggered | Can stop operation? | Triggered on | | -------------------- | -------------------------------------------------------- | :-----------------: | :----------: | | beforeServiceResolve | 在解析服务之前触发。监听器接收服务名称和传递给它的参数。 | No | Listeners | | afterServiceResolve | 在解析服务之后触发。监听器接收服务名称和传递给它的参数。 | No | Listeners | ## 共享服务 服务可以注册为“共享”服务,这意味着它们将始终是单例。一旦第一次解析服务,每次使用者从容器中检索服务时,都会返回相同的实例: ```php setShared( 'session', function () { $session = new SessionFiles(); $session->start(); return $session; } ); // 首次查找服务 $session = $di->get('session'); // 返回第一个实例化对象 $session = $di->getSession(); ``` 注册共享服务的另一种方法是将'true'作为'set'的第三个参数传递: ```php set( 'session', function () { // ... }, true ); ``` 如果服务未注册为共享,并且您希望确保每次从DI获取服务时都将访问共享实例,则可以使用“getShared”方法: ```php $request = $di->getShared('request'); ``` ## 单独操作服务 在服务容器中注册服务后,您可以检索它以单独操作它: ```php set('request', 'Phalcon\Http\Request'); // 获取服务 $requestService = $di->getService('request'); // 改变它的定义 $requestService->setDefinition( function () { return new Request(); } ); // 将其更改为共享 $requestService->setShared(true); // 解析服务 (return 一个 Phalcon\Http\Request 实例) $request = $requestService->resolve(); ``` ## 通过服务容器实例化类 当您向服务容器请求服务时,如果它找不到具有相同名称的服务,它将尝试加载具有相同名称的类。通过这种行为,我们可以通过使用其名称注册服务来替换另一个类: ```php set( 'IndexController', function () { $component = new Component(); return $component; }, true ); // 注册一个组件作为服务 $di->set( 'MyOtherComponent', function () { // 实际返回另一个组件 $component = new AnotherComponent(); return $component; } ); // 通过服务容器创建实例 $myComponent = $di->get('MyOtherComponent'); ``` 您可以利用这一点,始终通过服务容器实例化您的类(即使它们未注册为服务)。DI将回退到有效的自动加载器以最终加载该类。通过这样做,您可以通过实现它的定义轻松替换任何类。 ## 自动注入DI 如果类或组件需要DI本身来定位服务,DI可以自动将其自身注入到它创建的实例中,为此,您需要在类中实现`Phalcon\Di\InjectionAwareInterface`: ```php di = $di; } public function getDi() { return $this->di; } } ``` 然后一旦服务被解析,`$di`将自动传递给`setDi()`: ```php set('myClass', 'MyClass'); // 解析服务 (NOTE: $myClass->setDi($di) 会自动调用) $myClass = $di->get('myClass'); ``` ## 组织文件中的服务 您可以通过将服务注册移动到单个文件而不是在应用程序的引导程序中执行所有操作来更好地组织应用程序: ```php set( 'router', function () { return include '../app/config/routes.php'; } ); ``` 然后在文件(`'../app/config/routes.php'`)中返回已解析的对象: ```php post('/login'); return $router; ``` ## 以静态方式访问DI 如果需要,您可以通过以下方式访问在静态功能中创建的最新DI: ```php getSession(); } } ``` ## 服务提供者 使用`ServiceProviderInterface`,您现在可以按上下文注册服务。您可以将所有`$di->set()`调用移动到这样的类: ```php set( 'config', function () { return new Ini('config.ini'); } ); } } $di = new Di(); $di->register(new SomeServiceProvider()); var_dump($di->get('config')); // 将正确返回我们的配置 ``` ## Factory Default DI 尽管Phalcon的分离特性为我们提供了极大的自由和灵活性,但也许我们只是想将它用作全栈框架。为实现这一目标,该框架提供了一个名为`Phalcon\Di \FactoryDefault`的`Phalcon\Di`变体。此类自动注册与框架捆绑在一起的相应服务,以充当全栈。 ```php ';