buonaparte/bnp-service-definition

本模块提供了一种通过配置进行服务工厂定义的替代方案

1.0.3 2014-08-25 10:43 UTC

This package is not auto-updated.

Last update: 2024-09-24 03:13:12 UTC


README

Build Status Coverage Status Latest Stable Version Latest Unstable Version

此模块允许您通过简单但详尽的配置定义ServiceManager工厂。

变更日志

1.0.2

  • ParameterResolver现在正确考虑了顺序(测试覆盖率)

安装

设置

  1. 将此项目添加到您的composer.json文件中

    "require": {
        "buonaparte/bnp-service-definition": "1.*"
    }
  2. 现在运行以下命令,让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实例(这被称为定义参数)。

定义参数是一个简单的字符串或一个包含两个条目的数组:typevalue。参数用于指定服务类名、构造函数参数、要调用的方法名称以及它的参数和条件。默认情况下,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,仅接受intfloat / doublebooleanarray。!!! 注意,如果您想将数组作为参数传递,您必须使用FQ形式:['type' => 'value', 'value' => ['my_array_elements']]

  • dsl - 解析 value 键下的表达式,表达式必须是有效的 Symfony 表达式语言 语句。

每个参数都会通过 BnpServiceDefinition\Service\ParameterResolver 编译成 dsl 类型,用于评估或编译 configservice 类型,为此,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;
    }
}