定义服务的自动依赖(自动关联/Autowiring)

3.3 版本
维护中的版本

自动关联,允许你在容器中注册服务时,使用最简配置。它基于构造器的类型提示(type hint),自动解析服务的依赖。类型提示在 Rapid Application Development 领域是非常重要的,特别是在大型项目的早期原型开发阶段。它可以令服务注册和重构变得容易。

假设,你要构建一个API,用来发布Twttier订阅的状态,使用 ROT13 来加密(凯撒加密法的一种特例)。

由创建一个ROT13 transformer类开始:

1
2
3
4
5
6
7
8
9
namespace Acme;
 
class Rot13Transformer
{
    public function transform($value)
    {
        return str_rot13($value);
    }
}

现在,一个Twitter客户端要使用这个transformer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Acme;
 
class TwitterClient
{
    private $transformer;
 
    public function __construct(Rot13Transformer $transformer)
    {
        $this->transformer = $transformer;
    }
 
    public function tweet($user, $key, $status)
    {
        $transformedStatus = $this->transformer->transform($status);
 
        // ... connect to Twitter and send the encoded status
    }
}

现在,当 twitter_client 服务被标记为autowired之后,DependencyInjection组件能自动注册 TwitterClient 类的依赖:

1
2
3
4
services:
    twitter_client:
        class:    Acme\TwitterClient
        autowire: true
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="twitter_client" class="Acme\TwitterClient" autowire="true" />
    </services>
</container>
1
2
3
4
5
6
7
use Symfony\Component\DependencyInjection\Definition;
 
// ...
$definition = new Definition('Acme\TwitterClient');
$definition->setAutowired(true);
 
$container->setDefinition('twitter_client', $definition);

自动关联子系统,将通过解析 TwitterClient 类的构造器探测到其依赖。例如,它将找到这里有一个 Rot13Transformer 实例可以作为依赖。如果一个既有的服务定义(只有一个——见下文)正是所需要的类型,那么这个服务将被注入。但如果没有找到(如同本例代码),子系统将足够智能地自动帮 Rot13Transformer 类注册一个私有服务,然后把它设为 twitter_client 服务的第一个参数(注入)。再一次,子系统只工作在“有一个给定类型的类存在” 条件下。如果相同的类型有好几个类存在,你必须使用一个显式的服务定义(来完成DI)或者注册一个默认的实现(译注:$container->register('acme.your_service', default_class_FQCN))。

你已经看到,自动关联功能戏剧化地减少了定义一个服务时所需的配置信息。没有了参数选项!同时也令改变 TwitterClient 类的依赖变得容易:只需添加或删除构造器中的类型提示参数(typehinted arguments),然后一切搞定。没有更多的诸如查找和编辑相关服务定义的步骤。

下面是一个典型的使用了 twitter_client 服务的控制器:

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
28
29
30
namespace Acme\Controller;
 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
class DefaultController extends Controller
{
    /**
     * @Route("/tweet")
     * @Method("POST")
     */
    public function tweetAction(Request $request)
    {
        $user = $request->request->get('user');
        $key = $request->request->get('key');
        $status = $request->request->get('status');
 
        if (!$user || !$key || !$status) {
            throw new BadRequestHttpException();
        }
 
        $this->get('twitter_client')->tweet($user, $key, $status);
 
        return new Response('OK');
    }
}

你可以通过curl来给出API:

1
$  curl -d "user=kevin&key=ABCD&status=Hello" http://localhost:8000/tweet

它将返回OK

使用接口 

你可能已经发现,你正在使用抽象类来取代实现接口(特别是在近年来的程序中),因为它允许你轻松替换一些依赖,而毋须修改依赖它们的类。

为了遵循最佳实践,构造参数必须有接口级别的类型提示,而不使用具体化的类。这能令当前的implementation“在需要的时候被轻松替换”。同时还能使用其他的transformers。

现在我们引入一个 TransformerInterface

1
2
3
4
5
6
namespace Acme;
 
interface TransformerInterface
{
    public function transform($value);
}

然后编辑 Rot13Transformer,让它实现上面的新接口:

1
2
3
4
5
// ...
 
class Rot13Transformer implements TransformerInterface
 
// ...

再更新 TwitterClient 类,使其依赖这个新接口:

1
2
3
4
5
6
7
8
9
10
11
class TwitterClient
{
    // ...
 
    public function __construct(TransformerInterface $transformer)
    {
         // ...
    }
 
    // ...
}

最后把服务定义更新,因为很明显,自动关联子系统不能够找到它要自动注册的“实现了新接口的类”:

1
2
3
4
5
6
7
services:
    rot13_transformer:
        class: Acme\Rot13Transformer

    twitter_client:
        class:    Acme\TwitterClient
        autowire: true
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="rot13_transformer" class="Acme\Rot13Transformer" />
        <service id="twitter_client" class="Acme\TwitterClient" autowire="true" />
    </services>
</container>
1
2
3
4
5
6
7
8
9
use Symfony\Component\DependencyInjection\Definition;
 
// ...
$definition1 = new Definition('Acme\Rot13Transformer');
$container->setDefinition('rot13_transformer', $definition1);
 
$definition2 = new Definition('Acme\TwitterClient');
$definition2->setAutowired(true);
$container->setDefinition('twitter_client', $definition2);

现在,自动关联子系统已能够侦测到 rot13_transformer 服务实现了 TransformerInterface 接口,进而自动地注入它(到 twitter_client 服务)。甚至在使用接口时(你本当如此)、构造服务图表时、重构项目时,都比使用标准服务定义要容易。

操作同一类型(接口)的多个实现 

最后,同样重要的是,自动关联功能允许指定“给定类型”的默认实现。我们引入TransformerInterface接口的另外一个新实现,返回大写的ROT13 transformation结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Acme;
 
class UppercaseTransformer implements TransformerInterface
{
    private $transformer;
 
    public function __construct(TransformerInterface $transformer)
    {
        $this->transformer = $transformer;
    }
 
    public function transform($value)
    {
        return strtoupper($this->transformer->transform($value));
    }
}

这个类可以对任何一个transformer进行“装修”,返回大写的内容。

现在我们重构控制器,添加另外一个action以满足这个新的transformer:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
namespace Acme\Controller;
 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
class DefaultController extends Controller
{
    /**
     * @Route("/tweet")
     * @Method("POST")
     */
    public function tweetAction(Request $request)
    {
        return $this->tweet($request, 'twitter_client');
    }
 
    /**
     * @Route("/tweet-uppercase")
     * @Method("POST")
     */
    public function tweetUppercaseAction(Request $request)
    {
        return $this->tweet($request, 'uppercase_twitter_client');
    }
 
    private function tweet(Request $request, $service)
    {
        $user = $request->request->get('user');
        $key = $request->request->get('key');
        $status = $request->request->get('status');
 
        if (!$user || !$key || !$status) {
            throw new BadRequestHttpException();
        }
 
        $this->get($service)->tweet($user, $key, $status);
 
        return new Response('OK');
    }
}

最后一步就是更新服务定义了,注册这个新的实现,以便Twitter客户端能够使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services:
    rot13_transformer:
        class:            Acme\Rot13Transformer
        autowiring_types: Acme\TransformerInterface

    twitter_client:
        class:    Acme\TwitterClient
        autowire: true

    uppercase_rot13_transformer:
        class:    Acme\UppercaseRot13Transformer
        autowire: true

    uppercase_twitter_client:
        class:     Acme\TwitterClient
        arguments: ['@uppercase_rot13_transformer']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="rot13_transformer" class="Acme\Rot13Transformer">
            <autowiring-type>Acme\TransformerInterface</autowiring-type>
        </service>
        <service id="twitter_client" class="Acme\TwitterClient" autowire="true" />
        <service id="uppercase_rot13_transformer" class="Acme\UppercaseRot13Transformer" autowire="true" />
        <service id="uppercase_twitter_client" class="Acme\TwitterClient">
            <argument type="service" id="uppercase_rot13_transformer" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;
 
// ...
$definition1 = new Definition('Acme\Rot13Transformer');
$definition1->setAutowiringTypes(array('Acme\TransformerInterface'));
$container->setDefinition('rot13_transformer', $definition1);
 
$definition2 = new Definition('Acme\TwitterClient');
$definition2->setAutowired(true);
$container->setDefinition('twitter_client', $definition2);
 
$definition3 = new Definition('Acme\UppercaseRot13Transformer');
$definition3->setAutowired(true);
$container->setDefinition('uppercase_rot13_transformer', $definition3);
 
$definition4 = new Definition('Acme\TwitterClient');
$definition4->addArgument(new Reference('uppercase_rot13_transformer'));
$container->setDefinition('uppercase_twitter_client', $definition4);

这里要做一些解释。现在你有两个服务,实现的都是TransformerInterface。自动关联子系统无法猜中要把哪个做为依赖,并将导致下面这种错误:

1
2
[Symfony\Component\DependencyInjection\Exception\RuntimeException]
Unable to autowire argument of type "Acme\TransformerInterface" for the service "twitter_client".

幸运的是,autowiring_types 键专门用于指定默认条件下,使用哪个implementation。这个键还可以在必要时,接受一个“类型列表”。

多亏了这个选项,rot13_transformer 服务能够被自动注入到 uppercase_rot13_transformertwitter_client 服务中了。对于 uppercase_twitter_client,我们使用了一个标准的服务定义,来注入特定的 uppercase_rot13_transformer 服务。

至于其他的RAD功能,比如FrameworkBundle的控制器和annotations,记得不要在public bundles中使用自动关联功能,也不要在大规模、维护复杂的项目中使用。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)