end Framework 2 使用ServiceManager(简称SM)来实现控制反转(IoC)。有很多资料介绍了service managers的背景,我推荐大家看看this blog post from Evanthis post from Reese Wilson,但是仍然有很多开发者不能够很好地使用ServiceManager去解决他们的需求。这篇文章我将解释为什么ZF2框架需要使用多个服务管理器以及怎样使用它们。主要包含以下几个方面:

  1. 这些不同的服务管理器是什么?
  2. 不同的服务管理器用来干什么?
  3. 服务管理器与服务定位器是什么关系?
  4. 如何使用这些服务管理器定义服务?
  5. 如何在一个服务管理器中通过另一个服务管理器调用服务?

服务管理器使用在ZF2的许多地方,其中最重要的四个地方是:

  1. 应用全局服务管理(根服务管理器或者说是主要服务管理器)
  2. 控制器
  3. 控制器插件
  4. 视图助手

每一组功能都有一个服务管理器,这样做的好处是,可以使用同一个服务Key值指向不同的服务。假如有一个名为url的试图助手,也有一个名为url的控制器插件,如果只有一个服务管理器的话很难使用一个url Key值达到这个目的,而使用多个服务管理器可以轻松做到。
还有一个原因是出于安全考虑。假设有一个route向controller传递一个参数,通过此参数,服务管理器可以实例化相应的服务,如果你没有考虑安全问题,那么可以通过给一个服务管理器提供各种各样的参数从而实例化所有服务。

服务管理器与服务定位器的不同

很多人问ServiceLocator和ServiceManager有什么不同。ServiceLocator(简称SL)是一个接口:

namespace Zend\ServiceManager;
 
interface ServiceLocatorInterface
{
public function get($name);
public function has($name);
}

ServiceManager是ServiceLocator的一个具体实现。在zf2中SL的默认实现是SM。在整个框架中,有时会看到 getServiceLocator()方法,而有时会看到getServiceManager()方法。getServiceLocator()获得的 SL接口,而getServiceManager获得的是具体的SM实现。

两者之间并没有很大的区别,因为他们通常返回的是同一个对象。但是有时候一个SL可以有多个不同SM实现,许多zf2组件需要明确指定一个实现。

配置服务管理器

两种方法可以配置服务管理器:1.module类本身可以return SM配置; 2.模块配置文件(通常是config/module.config.php)可以return SM配置。两种方法功能是一样的,只是看你自己喜欢放置到哪儿。
你可以使用下面任意一种方法添加服务:

/**
* 在module class类本身
*/
namespace MyModule;
 
class Module
{
public function getServiceConfig()
{
return array(
'invokables' => array(
'my-foo' => 'MyModule\Foo\Bar',
),
);
}
}
/**
* 在module config中
*/
return array(
'service_manager' => array(
'invokables' => array(
'my-foo' => 'MyModule\Foo\Bar'
),
),
);

我们看到,两种不同的方法中返回的数组都是一样的,四种类型的服务管理器都是这样的。在module类中,你只需要实现getServiceConfig方法,配置就会被加载,使用的是duck type模式(不一定要继承,只要他们方法一样,就认为他们是一回事。例如:有一只鸟,如果它像鸭子一样叫,像鸭子一样游泳,像鸭子一样走路,就认为它就是一只鸭子)。如果你想严格规范这个方法,也可以添加一个接口。例如:

namespace MyModule;
 
use Zend\ModuleManager\Feature\ServiceProviderInterface;
 
class Module implements ServiceProviderInterface
{
public function getServiceConfig()
{
return array(
'invokables' => array(
'my-foo' => 'MyModule\Foo\Bar',
),
);
}
}

四种服务管理器,你都可以添加一个Key到模块配置文件或者添加一个方法到模块类。对于后者,你可以duck type一些方法也可以添加一个新的接口在Zend\MoudleManager\Feature\*interface。下面的列表反映了他们之间的联 系。“manager”代表管理什么,还提供了管理器类名、模块配置数组中的Key、模块的方法和接口。对于controller、controller plugin、view helper管理器,在全局管理器service manger中注册服务时指定了service name(服务名称)。

Manager: Application services

  • Manager class: Zend\ServiceManager\ServiceManager
  • Config key: service_manager
  • Module method: getServiceConfig()
  • Module interface: ServiceProviderInterface

Manager: Controllers

  • Manager class: Zend\Mvc\Controller\ControllerManager
  • Config key: controllers
  • Module method: getControllerConfig()
  • Module interface: ControllerProviderInterface
  • Service name: ControllerLoader

Manager: Controller plugins

  • Manager class: Zend\Mvc\Controller\PluginManager
  • Config key: controller_plugins
  • Module method: getControllerPluginConfig()
  • Module interface: ControllerPluginProviderInterface
  • Service name: ControllerPluginManager

Manager: View helpers

  • Manager class: Zend\View\HelperPluginManager
  • Config key: view_helpers
  • Module method: getViewHelperConfig()
  • Service name: ViewHelperManager

需要注意的是
有一关键点我们需要注意,正如Evan解释,对于一个工厂类有两个选项,要么是一个闭包,要么是一个字符串指向的类。这个类必须实现Zend\ServiceManager\FactoryInterface接口,或者它必须有__invoke方法。这个工厂将被放置到模块配置文件中,或者模块类中。

如果模块配置文件中使用闭包,就会有问题,因为所有的模块配置文件都将缓存到一个大的合并后的配置文件中,然而PHP中的闭包不能被序列化,不能被合并后缓存。所以你要么在模块配置文件中使用工厂类,要么使用getServiceConfig()方法。

根服务管理器与其他管理器的比较

根(root)通常在讨论IRC时使用,好像它是基础代码一样,但是实际上它与zf2的基础代码不是毫无关联。“根服务管理器”这个名字的也许来自
于:Zend\ServiceManager\ServiceManager控制着所有主要的服务,而其他的服务管理器只专注于一种服务。“根”这个名字
好像暗示着它与其他一些managers有着某种关系。猜一猜是不是这样呢?确实,有一种联系存在。

假设你有一个controller,需要注入一个cache(缓存实例)进去。controller在controller service
manager中具有缓存实例的工厂factory,缓存是root service
manager的一个service。在controller服务管理器的工厂中如何获得缓存服务?这就是root service
manager(根服务管理器)与其他服务管理器的关联之处。controller、controller plugin、view
helper的service
manager都是AbstractPluginMangaer抽象类的实现(Implementation),这个类有一个方法
getServiceLocator()能够返回root service manager,这使得各种不同的服务管理器能够来回调用:

/*在Module.config.php中*/
use MyModule\Controller;
 
return array(
'controllers' => array(
'factories' => array(
'MyModule\Controller\Foo' => function($sm) {
$controller = new Controller\FooController;
 
$cache = $sm->getServiceLocator()->get('my-cache');
$controller->setCache($cache);
 
return $controller;
},
),
),
);

这里cache服务通过root service locator(根服务定位器)获得,通过$sm->getServiceLocator()可以获得任何根服务管理器下的服务。

如果你知道controller plugin manager和view helper manager 是注册在root service locator的话,这将变得非常有趣。你可以轻松的在一个服务中注入一个运行时对象到view helper中。例如,在url view helper(服务)中注入router(对象),这个对象对于使用route名字来组装url是必须的。

你可以通过“ControllerPluginManager”这个Key从根服务管理器(root SM)中获得controller plugin manager,view helper manager对应的Key是“ViewHelperManager”,你可以像这样获得一个插件:

use MyModule\Service;
 
return array(
'service_manager' => array(
'factories' => array(
'MyModule\Service\Foo' => function($sm) {
$service = new Service\Foo;
 
$plugins = $sm->get('ViewHelperManager');
$plugin = $plugins->plugin('my-plugin');
$service->setPlugin($plugin);
 
return $service;
},
),
),
);

点对点的(peering) service manager

点对点service manager的概念很简单,就是说controller plugin和view helper service manager从root SM调用其他服务时不适用$sm->getServiveLocator()。点对点(peering)的主要意思是,controller plugin SM 加载自己的服务失败后再从root SM中加载服务。

因此,看上面的例子,在某种场合下,你可以跳过$sm->getServiceLocator(),直接获取服务。这只适用于 controller plugins和view helpers,对于controller SM是不适用的。原因很显然,controller SM有一个安全问题:你有可能由于请求了一个特俗的URL而意外地实例化了一个对象。如果你允许controller SM点对点获取服务的话,你将导致安全漏洞。尽管这样但是对于controller plugin和view helper,点对点仍然是有价值的。

use MyModule\Controller\Plugin;
 
return array(
'controller_plugins' => array(
'factories' => array(
'MyModule\Controller\Plugin\Foo' => function($sm) {
$plugin = new Plugin\Foo;
 
$cache = $sm->get('my-cache');
$plugin->setCache($cache);
 
return $plugin;
},
),
),
);

这么做的好处就是对于controller plugin和view helper,你可以忽略getServiceLocator(),这使得你的代码更加易读。在字里行间你可能读到了我的担忧:点对点并不是很容易掌握。 在上面的例子中,$sm并没有“my-cache”这个服务,但是你尝试去获取这个服务,你将得到cache。(这个地方不是很明白)。最好对这个工厂做 好文档,否则以后将会遇到麻烦。

个人喜好

我更加喜欢在Module中使用严格的接口。我总是使用Zend\ModuleManager\Feature interfaces,我总是把所有的service的配置放到一个config文件中,使用闭包作为工厂,这使得我可以清楚看到一个module中所有 的service key,而不是混杂着route config(从module config文件)或者 autoload config 或者 bootstrap 逻辑(从Module类)。

通常在module.config.php同目录旁边放置一个servcie.config.php文件在config/目录下面,然后include这个文件就像include module配置文件一样。Module类通常像这样:

namespace MyModule;
 
use Zend\Loader;
use Zend\ModuleManager\Feature;
use Zend\EventManager\EventInterface;
 
class Module implements
Feature\AutoloaderProviderInterface,
Feature\ConfigProviderInterface,
Feature\ServiceProviderInterface,
Feature\BootstrapListenerInterface
{
public function getAutoloaderConfig()
{
return array(
Loader\AutoloaderFactory::STANDARD_AUTOLOADER => array(
Loader\StandardAutoloader::LOAD_NS => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
),
),
);
}
 
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
 
public function getServiceConfig()
{
return include __DIR__ . '/config/service.config.php';
}
 
public function onBootstrap(EventInterface $e)
{
// Some logic
}
}

module.config.php文件提供一些基础配置,service.config.php把所有的服务整合到一起。通过EnsembleKernel这个例子可以了解这种配置方式,其中service.config.php看起来像这样。当然,也有一些别的方法能够处理的非常好,看你个人喜好了。

英文原文链接 Using Zend Framework service managers in your application

本文是作者的团队博客ComingXZend Framework 2中如何使用Service Manager 文章的一份拷贝,同为原创文章。

最新文章

  1. Extjs使用Ext.function.bind, 给句柄函数传参
  2. BestCoder#16 A-Revenge of Segment Tree
  3. MongoDB的安装小结
  4. html5 之 canvas 相关知识(一)概念及定义
  5. MySQL内存体系架构及参数总结 ---图解
  6. 2014年百度之星资格赛第三题Xor Sum
  7. [翻译] EF Core in Action 关于这本书
  8. 微信小程序登录流程及解析用户openid session_key,获取用户信息
  9. js中获取当前项目名等
  10. 计算器和Menu
  11. 【debug、info、warn、error】四者之间的区别与用法
  12. windows 服务器MYSQL 数据库安装配置
  13. LCT摘要
  14. spring 注解实例
  15. Node.js框架之Egg.js
  16. Linux日志文件/var/log详解
  17. 【扩展欧几里得】BZOJ1477-青蛙的约会
  18. 洛谷p1208 水题贪心 思想入门
  19. codevs 1966 乘法游戏
  20. sql注意事项积累

热门文章

  1. I/O操做总结(四))
  2. JavaScript中var a=b=c=d的发现
  3. Linux生产服务器常规分区方案
  4. Android GreenDao操作外部DB数据库文件
  5. web APP到底和跨平台APP开发有什么区别?
  6. CAS 配置NLB 负载均衡网络无法连接
  7. JavaScript空假值及其判断
  8. HTML之基本语法(链接标签、路径的介绍和使用)
  9. python_101_类方法
  10. java8关于LocalDate,Date