在Symfony程序中,某些服务需要访问若干其他服务,尽管那些服务并没有被真正用到(如 FirewallMap 类)。把所有这些未用到的服务进行实例化是无用的,但又不可能通过显式地依赖注入把它们变成lazy services。

这种场合下的传统解决方案是,注入整个服务容器,以便只获取到真正所需之服务。然而,并不推荐这样做,因为这给了服务一个过于宽泛的访问程序其他部分的机会,却隐藏了这些服务真正依赖。

Service locators 是一种设计模式,"encapsulate the processes involved in obtaining a service [...] using a central registry known as the service locator"(封装那些涉及到获取某个服务的进程[...],使用一个集中登记处,即service locator)。这个模式一般不被提倡,但在此处的场合却十分有用,它这种方式好过注入整个容器。

思考 CommandBus 类,它映射了命令行及其handlers。这个类一次只处理一条命令,因此将它们全部实例化并没有用。首先,定义一个service locator服务,使用全新的 container.service_locator 标签,再把全部服务作为该服务的参数:

1
2
3
4
5
6
7
8
9
# app/config/services.yml
services:
    app.command_handler_locator:
        class: Symfony\Component\DependencyInjection\ServiceLocator
        tags: ['container.service_locator']
        arguments:
            -
                AppBundle\FooCommand: '@app.command_handler.foo'
                AppBundle\BarCommand: '@app.command_handler.bar'

然后,注入service locator到定义为command bus的服务之中:

1
2
3
4
# app/config/services.yml
services:
    AppBundle\CommandBus:
        arguments: ['@app.command_handler_locator']

被注入的service locator是一个 Symfony\Component\DependencyInjection\ServiceLocator 的实例。这个类实现的是PSR-11 ContainerInterface 接口,该接口包括了 has()get() 方法以从locator检查并获取服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ...
use Psr\Container\ContainerInterface;
 
class CommandBus
{
    /** @var ContainerInterface */
    private $handlerLocator;
 
    // ...
 
    public function handle(Command $command)
    {
        $commandClass = get_class($command);
 
        // check if some service is included in the service locator
        // 检查是否有服务被包容到service locator中
        if (!$this->handlerLocator->has($commandClass)) {
            return;
        }
 
        // get the service from the service locator (and instantiate it)
        // 从service locator中获取服务(并实例化之)
        $handler = $this->handlerLocator->get($commandClass);
 
        return $handler->handle($command);
    }
}