buonaparte / bnp-service-definition
本模块提供了一种通过配置进行服务工厂定义的替代方案
Requires
Requires (Dev)
- phpunit/phpunit: >=3.7,<4
- satooshi/php-coveralls: dev-master
- squizlabs/php_codesniffer: 1.4.*
This package is not auto-updated.
Last update: 2024-09-24 03:13:12 UTC
README
此模块允许您通过简单但详尽的配置定义ServiceManager工厂。
变更日志
1.0.2
ParameterResolver现在正确考虑了顺序(测试覆盖率)
安装
设置
-
将此项目添加到您的composer.json文件中
"require": { "buonaparte/bnp-service-definition": "1.*" }
-
现在运行以下命令,让composer下载BnpServiceDefinition:
$ php composer.phar update
安装后
在您的application.config.php文件中启用它。
<?php return array( 'modules' => array( // ... 'BnpServiceDefinition', ), // ... );
配置
通过复制和调整config/bnp-service-definition.global.php.dist到您的配置包含路径来配置模块。
定义
在默认的ZF2应用程序中,您将通过工厂定义所有依赖项,为每个服务编写一个Factory类通常会花费很多时间并成为额外的开销,为了快速原型开发,开发者通常使用闭包来定义工厂 - 闭包的问题在于它们不能被缓存。许多Zf2开发者使用Zend\Di进行原型设计,然而这个工具具有更大的开销,并且魔法太多。BnpServiceDefinition提出了一种通过所有“喜爱的”数组配置来定义工厂的替代方法,直接在模块配置中。所有定义都由数组表示,遵循以下结构(我们将使用简短的数组语法,但此模块没有对PHP 5.4的依赖)
return [ // ... 'service_manager' => [ // ... 'definitions' => [ 'MovieLister' => [ 'class' => 'MyApp\Service\MovieLister', 'arguments' => [ ['type' => 'service', 'value' => 'MovieFinder'] ], 'method_calls' => [ ['name' => 'setListingBehaviour', 'parameters' => ['default']] ] ], 'MovieFinder' => [ 'class' => 'MyApp\Service\MovieFinder', 'arguments' => [ ['type' => 'service', 'value' => 'MoviesTable'] ] ], 'MoviesTable' => [ 'class' => 'Zend\Db\TableGateway\TableGateway', 'arguments' => [ 'movies', ['type' => 'service', 'value' => 'Zend\Db\Adapter'], null, ['type' => 'service', 'value' => 'MoviesResultSet'] ] ], 'MoviesResultSet' => [ 'class' => 'Zend\Db\ResultSet\HydratingResultSet', 'arguments' => [ ['type' => 'service', 'value' => 'ClassMethodsHydrator'], ['type' => 'service', 'value' => 'MovieEntityPrototype'], ] ] ], 'invokables' => [ 'ClassMethodsHydrator' => 'Zend\Stdlib\Hydrator\ClassMethods', 'MovieEntityPrototype' => 'MyApp\Entity\MovieEntity' ], 'shared' => [ 'MovieEntityPrototype' => false ] ] ];
上面的示例说明了一个非常简单的MovieLister服务定义。注意在您的service_manager配置下有一个额外的键。每个服务定义都可以包含以下内容
- class - 服务类名
- arguments - 传递给服务构造函数的参数,称为“硬依赖”
- method_calls - 在返回服务之前对服务进行的任何附加方法调用,例如setter注入或初始化任务
MovieLister服务是MyApp\Service\MovieLister的实例,具有一个单一的构造函数参数,语法相当奇怪,一个数组['type' => 'service', 'value' => 'MovieFinder'],这告诉定义解析器在应用程序服务定位器中查找MovieFinder实例(这被称为定义参数)。
定义参数是一个简单的字符串或一个包含两个条目的数组:type和value。参数用于指定服务类名、构造函数参数、要调用的方法名称以及它的参数和条件。默认情况下,BnpServiceDefinition提供了以下可解析的参数类型
-
config - 通过从
Config共享服务中指定的value获取配置值,value可以是字符串或指向嵌套配置的数组,例如:['parameters', 'some_parameter']将返回$config['parameters']['some_parameter']或null如果找不到配置值。 -
service - 通过名称获取服务,指定由ServiceManager中指定的
value,如果服务未定义或无法创建,则为null,例如:'Zend\Log'将返回$serviceLocator->get('Zend\Log')实例。 -
value - 将参数按原样传递,定义为
value键下的value,仅接受int、float/double、boolean和array。!!! 注意,如果您想将数组作为参数传递,您必须使用FQ形式:['type' => 'value', 'value' => ['my_array_elements']]。 -
dsl - 解析
value键下的表达式,表达式必须是有效的 Symfony 表达式语言 语句。
每个参数都会通过 BnpServiceDefinition\Service\ParameterResolver 编译成 dsl 类型,用于评估或编译 config 和 service 类型,为此,Symfony 表达式语言 扩展了 2 个函数。
service(service_name, silent = false, instance = null)
config(string_or_array_for_nested_config_path, silent = true, type = null)
假设 MovieLister 行为将从数据库中检索,则 method_call 定义可能变为
// ... 'method_calls' => [ [ 'name' => 'setListingBehaviour', 'parameters' => [ ['type' => 'dsl', 'value' => 'service("PreferencesMapper").getDefaultListingBehaviour()'] ] ] ]
方法调用也支持条件,因此,当所有条件都评估为真时,将调用此方法,每个条件也是一个 定义参数,因此以下方式是完全合法的。
// ... 'method_calls' => [ [ 'name' => 'setListingBehaviour', 'parameters' => [ ['type' => 'dsl', 'value' => 'service("PreferencesMapper").getDefaultListingBehaviour()'] ], 'conditions' => [ ['type' => 'dsl', 'value' => 'service("UserSession").hasDefaultListingSpecified()'] ] ] ]
有许多情况,当我们的某些服务具有相同的构造函数参数,或者其中一部分是相同的。由于使用抽象工厂可能不是最佳选择或根本不可能,您可以将重复的服务工厂内容定义为 抽象 定义,而所有具体工厂都将指定为 父类(父类是递归解决的)。
'definitions' => [ 'DbAdapterDependentService' => [ 'arguments' => [ ['type' => 'service', 'value' => 'Zend\Db\Adapter'] ], 'abstract' => true, // suppose all of them will implement Zend\Stdlib\InitializableInterface 'method_calls' => [ 'init' ] ], 'UserMapper' => [ 'class' => 'MyApp\Mapper\UserMapper', 'parent' => 'DbAdapterDependentService' ], 'SettingsMapper' => [ 'class' => 'MyApp\Mapper\SettingsMapper', 'parent' => 'DbAdapterDependentService', 'arguments' => [ ['type' => 'config', 'value' => 'a_config_value', 'order' => -1] ] ] ]
注意参数的 order 键,这是可选的,默认情况下,所有参数都分配 0 的顺序。然而,在编译时,所有参数将按照此键值的升序进行排序,SettingsMapper 的第一个构造函数参数将是从配置中拉取的值。
使用 PluginManager 范围内的定义
您可以通过在 bnp-service-definition 配置键下的 definition-aware-containers 中指定它,为每个 Plugin Manager 添加 definitions 支持。例如:
'bnp-service-definition' => [ 'definition-aware-containers' => [ 'ControllerManager' => 'controller_manager', ] ]
!!! 注意 此范围中使用的服务类型参数或服务 dsl 函数将指向 ZF2 的应用程序服务管理器,要访问当前范围的插件,您可以使用此 dsl 语法:service('ControllerManager').get('some_service')。
假设一个 MoviesController,我们现在可以将 MovieLister 服务作为硬依赖项注入
'controller_manager' => [ 'definitions' => [ 'MoviesController' => [ 'class' => 'MyApp\Controller\MoviesController', 'arguments' => [ ['type' => 'service', 'value' => 'MovieLister'] ] ] ] ]
它的工作原理
在应用程序引导事件期间,BnpServiceDefinition 模块将向应用程序的服务管理器注册一个额外的抽象工厂,同时,将读取 bnp-service-definition 配置键下的 definition-aware-containers 并为指定的每个容器注册一个抽象工厂实例。该抽象工厂将在它所属的服务管理器配置键下查找 definitions 键,并负责动态创建或委派创建所有“终端”(不包含 'abstract' => true)定义的编译版本。
如果 bnp-service-definition 下的 dump-abstract-factories 设置为 true,则抽象工厂将委派其所有调用到编译(转储)版本,或者,如果需要,将每个请求的定义编译为 Symfony 表达式语言 并即时评估。
出于性能考虑,您将始终将 dump-abstract-factories 设置为 true,模块将检查您的定义是否已更改,并在需要时动态生成编译版本,您唯一要关心的是指定一个可写目录来存储这些抽象工厂,例如:./data/bnp-service-definitions。
为 MovieLister 服务示例生成的转储版本将保存在一个名为 BnpGeneratedAbstractFactory_a81f0487f49ba10e22972a55497525bc.php 的文件中,内容如下:
/** * Generated by BnpServiceDefinition\Service\Generator (at 10:40 25-08-2014) */ class BnpGeneratedAbstractFactory_a81f0487f49ba10e22972a55497525bc implements \Zend\ServiceManager\AbstractFactoryInterface, \Zend\ServiceManager\ServiceLocatorAwareInterface { /** * @var \Zend\ServiceManager\ServiceLocatorInterface */ protected $services = null; /** * @var string */ protected $scopeLocatorName = null; /** * Constructor * * @param string $scopeLocatorName */ public function __construct($scopeLocatorName = null) { $this->scopeLocatorName = $scopeLocatorName; } /** * Determine if we can create a service with name * * @param \Zend\ServiceManager\ServiceLocatorInterface $serviceLocator * @param string $name * @param string $requestedName * @return bool */ public function canCreateServiceWithName(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocator, $name, $requestedName) { return in_array($requestedName, array('MovieLister', 'MovieFinder', 'MoviesTable', 'MoviesResultSet')); } /** * Create service with name * * @param \Zend\ServiceManager\ServiceLocatorInterface $serviceLocator * @param string $name * @param string $requestedName * @return mixed */ public function createServiceWithName(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocator, $name, $requestedName) { switch ($requestedName) { case 'MovieLister': return $this->getMovieLister('MovieLister'); case 'MovieFinder': return $this->getMovieFinder('MovieFinder'); case 'MoviesTable': return $this->getMoviesTable('MoviesTable'); case 'MoviesResultSet': return $this->getMoviesResultSet('MoviesResultSet'); } return null; } /** * Set service locator * * @param Zend\ServiceManager\ServiceLocatorInterface $serviceLocator */ public function setServiceLocator(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocator) { $this->services = $serviceLocator; } /** * Get service locator * * @return \Zend\ServiceManager\ServiceLocatorInterface */ public function getServiceLocator() { return $this->services; } /** * Returns the service registered under "MovieLister" definition * * @param string $definitionName * @return object * @throws \BnpServiceDefinition\Exception\RuntimeException If an error occurs * during instantiation */ protected function getMovieLister($definitionName) { set_error_handler( function ($level, $message) use ($definitionName) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( 'A %d level error occurred (message: "%s") while creating %s service from compiled Abstract Factory', $level, $message, $definitionName )); } ); $serviceClassName = "MyApp\\Service\\MovieLister"; if (! is_string($serviceClassName)) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( '%s definition class was not resolved to a string', $definitionName )); } if (! class_exists($serviceClassName, true)) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( '%s definition resolved to the class %s, which does no exit', $definitionName, $serviceClassName )); } $serviceReflection = new \ReflectionClass($serviceClassName); $service = $serviceReflection->newInstanceArgs(array($this->services->get('BnpServiceDefinition\Dsl\Extension\ServiceFunctionProvider')->getService("MovieFinder", false, null))); $serviceMethod = "setListingBehaviour"; if (! is_string($serviceMethod)) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( 'A method call can only be a string, %s provided, as %d method call for the %s service definition', gettype($serviceMethod), 0, $definitionName )); } elseif (! method_exists($service, $serviceMethod)) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( 'Requested method "%s::%s" (index %d) does not exists or is not visible for %s service definition', get_class($service), $serviceMethod, 0, $definitionName )); } call_user_func_array( array($service, $serviceMethod), array("default") ); restore_error_handler(); return $service; } /** * Returns the service registered under "MovieFinder" definition * * @param string $definitionName * @return object * @throws \BnpServiceDefinition\Exception\RuntimeException If an error occurs * during instantiation */ protected function getMovieFinder($definitionName) { set_error_handler( function ($level, $message) use ($definitionName) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( 'A %d level error occurred (message: "%s") while creating %s service from compiled Abstract Factory', $level, $message, $definitionName )); } ); $serviceClassName = "MyApp\\Service\\MovieFinder"; if (! is_string($serviceClassName)) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( '%s definition class was not resolved to a string', $definitionName )); } if (! class_exists($serviceClassName, true)) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( '%s definition resolved to the class %s, which does no exit', $definitionName, $serviceClassName )); } $serviceReflection = new \ReflectionClass($serviceClassName); $service = $serviceReflection->newInstanceArgs(array($this->services->get('BnpServiceDefinition\Dsl\Extension\ServiceFunctionProvider')->getService("MoviesTable", false, null))); restore_error_handler(); return $service; } /** * Returns the service registered under "MoviesTable" definition * * @param string $definitionName * @return object * @throws \BnpServiceDefinition\Exception\RuntimeException If an error occurs * during instantiation */ protected function getMoviesTable($definitionName) { set_error_handler( function ($level, $message) use ($definitionName) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( 'A %d level error occurred (message: "%s") while creating %s service from compiled Abstract Factory', $level, $message, $definitionName )); } ); $serviceClassName = "Zend\\Db\\TableGateway\\TableGateway"; if (! is_string($serviceClassName)) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( '%s definition class was not resolved to a string', $definitionName )); } if (! class_exists($serviceClassName, true)) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( '%s definition resolved to the class %s, which does no exit', $definitionName, $serviceClassName )); } $serviceReflection = new \ReflectionClass($serviceClassName); $service = $serviceReflection->newInstanceArgs(array("movies", $this->services->get('BnpServiceDefinition\Dsl\Extension\ServiceFunctionProvider')->getService("Zend\\Db\\Adapter", false, null), null, $this->services->get('BnpServiceDefinition\Dsl\Extension\ServiceFunctionProvider')->getService("MoviesResultSet", false, null))); restore_error_handler(); return $service; } /** * Returns the service registered under "MoviesResultSet" definition * * @param string $definitionName * @return object * @throws \BnpServiceDefinition\Exception\RuntimeException If an error occurs * during instantiation */ protected function getMoviesResultSet($definitionName) { set_error_handler( function ($level, $message) use ($definitionName) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( 'A %d level error occurred (message: "%s") while creating %s service from compiled Abstract Factory', $level, $message, $definitionName )); } ); $serviceClassName = "Zend\\Db\\ResultSet\\HydratingResultSet"; if (! is_string($serviceClassName)) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( '%s definition class was not resolved to a string', $definitionName )); } if (! class_exists($serviceClassName, true)) { throw new \BnpServiceDefinition\Exception\RuntimeException(sprintf( '%s definition resolved to the class %s, which does no exit', $definitionName, $serviceClassName )); } $serviceReflection = new \ReflectionClass($serviceClassName); $service = $serviceReflection->newInstanceArgs(array($this->services->get('BnpServiceDefinition\Dsl\Extension\ServiceFunctionProvider')->getService("ClassMethodsHydrator", false, null), $this->services->get('BnpServiceDefinition\Dsl\Extension\ServiceFunctionProvider')->getService("MovieEntityPrototype", false, null))); restore_error_handler(); return $service; } }