服务容器

现代php程序全部是对象。一个对象可以负责电子邮件的发送,另一个对象能让你把信息持久化到数据库中。在程序中你可以建立一个对象,用来管理产品库存,或者用另一个对象处理第三方API中的数据。结论是,现代程序可以做许多事,而程序是由许多组织在一起的处理各自任务的对象构成。

本章讲的是Symfony中一个特殊的PHP对象,它帮助你实例化、组织和取出程序中的这许多对象。这个对象,被称为“服务容器”,它允许你在程序中把对象的组织方式变得标准化与集中化。容器令事情变得简单,它非常快,而且强调从架构上提升代码的复用性同时降低藕合性。由于Symfony所有的类都要使用容器,你将学习到如何扩展、配置与使用Symfony中的对象。从大的方面讲,服务容器是Symfony的速度与扩展性的最大功臣。

最后,配置与使用服务容器很简单。学完本章,你能通过服务容器轻松创建自己的对象,也能自定义第三方bundle中的任何对象。你将能够开始书写可复用、可测试、松藕合的代码,其原因就在于服务容器令编写良好代码变得容易。

如果你在读完本章还想了解更多,请参考依赖注入组件

什么是服务 

简单地说,服务(Service)可以是任何执行“全局”任务的对象。它在计算机科学中是一个专有名词,用来形容一个“为完成某种使命(比如发送邮件)”而被创建的对象。在程序中,当你需要某个服务所提供的特定功能时,可以随时取用该服务。创建服务时你不需要做任何特殊的事:只要写一个PHP类,令其完成某个任务即可。恭喜,你已经创建了一个服务!

作为原则,一个PHP对象若要成为服务,必须能在程序的全局范围使用。一个独立的Mailer服务被“全局”用于发送邮件信息,然而它所传输的这些Message信息对象并是服务。类似的,一个Product对象,并不是服务,但是一个能够把产品持久化到数据库中的对象,就是服务。

这说明了什么?以“服务”角度思考问题的好处在于,你已经开始想要把程序中的每一个功能给分离出来,形成一系列服务。由于每个服务只做一件事,你可以在任何需要的时候轻松访问到这个服务。对每个服务的测试和配置变得更加容易,因为它们已经从你程序中的其他功能性中分离出来。这个理念被称为面向服务架构,并非Symfony或PHP专有。把你的程序通过一组独立存在的服务类进行“结构化”,是久经考验且广为人知的面向对象编程之最佳实践。这个技巧可谓是成为任何一门语言的优秀开发者的关键。

什么是服务容器 

服务容器(Service Container/dependency injection container)就是一个PHP对象,它管理服务(即对象)的实例化。

举例来说,假设你有一个简单的类,用于发送邮件信息。如果没有服务容器,你必须在需要的时候手动创建对象。

1
2
3
4
use Acme\HelloBundle\Mailer;
 
$mailer = new Mailer('sendmail');
$mailer->send('ryan@example.com', ...);

这很简单。一个虚构的Mailer邮件服务类,允许你配置邮件的发送方法(比如sendmail,或smtp,等等)。但如果你想在其他地方使用邮件服务怎么办?你当然不希望每次都重新配置Mailer对象。如果你想改变邮件的传输方式,把整个程序中所有的sendmail改成smtp怎么办?你不得不找到所有创建了Mailer的地方,手动去更新。

在容器中创建和配置服务 

上面问题的最佳答案,是让服务容器来为你创建Mailer对象。为了让容器正常工作,你先要教会它如何创建Mailer服务。这是通过配置来实现的,配置方式有YAML,XML或PHP:

1
2
3
4
5
# app/config/services.yml
services:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    [sendmail]
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- app/config/services.xml -->
<?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="app.mailer" class="AppBundle\Mailer">
            <argument>sendmail</argument>
        </service>
    </services>
</container>
1
2
3
4
5
6
7
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
 
$container->setDefinition('app.mailer', new Definition(
    'AppBundle\Mailer',
    array('sendmail')
));

当Symfony初始化时,它要根据配置信息来建立服务容器(默认配置文件是app/config/config.yml)。被加载的正确配置文件是由AppKernel::registerContainerConfiguration()方法指示,该方法加载了一个“特定环境”的配置文件(如config_dev.yml是dev开发环境的,而config_prod.yml则是生产环境的)。

一个Acme\HelloBundle\Mailer对象的实例已经可以通过服务容器来使用了。容器在任何一个标准的Symfony控制器中可以通过get()快捷方法直接获得。

1
2
3
4
5
6
7
8
9
10
11
class HelloController extends Controller
{
    // ...
 
    public function sendEmailAction()
    {
        // ...
        $mailer = $this->get('app.mailer');
        $mailer->send('ryan@foobar.net', ...);
    }
}

当你从容器中请求my_mailer服务时,容器构造了该对象并返回(实例化之后的)它。这是使用服务容器的又一个好处。即,一个服务不会被构造(constructed),除非在需要时。如果你定义了一个服务,但在请求(request)过程中从未用到,该服务不会被创建。这可节省内存并提高程序运行速度。这也意味着在定义大量服务时,很少会对性能有冲击。从不使用的服务绝对不会被构造。

附带一点,Mailer服务只被创建一次,每次你请求它时返回的是同一实例。这可满足你的大多数需求(该行为灵活而强大),但是后面你要了解怎样才能配置一个拥有多个实例的服务,参考如何定义非共享服务cookbook文章。

本例中,控制器继承了Symfony的Controller基类,给了你一个使用服务容器的机会,通过get()方法就可从容器中锁定并取出my_mailer服务。你也可以把controller定义成服务(controllers as services)。这是一个先进但不一定必要的玩法,它允许你只把所需的服务注入到控制器中。

服务参数 

通过容器建立新服务的过程十分简单明了。参数(Parameter)可以令服务的定义更加灵活、有序:

1
2
3
4
5
6
7
8
# app/config/services.yml
parameters:
    app.mailer.transport:  sendmail

services:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    ['%app.mailer.transport%']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- app/config/services.xml -->
<?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">
 
    <parameters>
        <parameter key="app.mailer.transport">sendmail</parameter>
    </parameters>
 
    <services>
        <service id="app.mailer" class="AppBundle\Mailer">
            <argument>%app.mailer.transport%</argument>
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
 
$container->setParameter('app.mailer.transport', 'sendmail');
 
$container->setDefinition('app.mailer', new Definition(
    'AppBundle\Mailer',
    array('%app.mailer.transport%')
));

结果就和之前一样。区别在于,你是如何定义的服务。通过把my_mailer.transport%百分号给括起来,容器就知道应该去找对应这个名字的参数。容器自身生成时,会把每个参数的值,还原到服务定义中。

如果你要把一个由@开头的字符串,在YAML文件中用做参数值的话(例如一个非常安全的邮件密码),你需要添加另一个@符号进行转义(这种情况只在YAML格式的配置文件中适用)

1
2
3
4
# app/config/parameters.yml
parameters:
    # This will be parsed as string '@securepass'
    mailer_password: '@@securepass'

配置参数(parameter)或方法参数(argument)中的百分号,也必须用另一个%进行转义:

1
<argument type="string">http://symfony.com/?foo=%%s&bar=%%d</argument>

参数的目的,是要把信息传给服务。当然,不用参数的话,也不会有什么问题。但使用参数有以下几个好处:

  • 分离并组织所有服务“选项”到一个统一的“参数”键下

  • 参数值可以被用到多个服务定义中

  • 在bundle中创建服务时,使用参数可以令服务在全局程序中的定制变得容易

是否使用参数,选择权在你。高质量第三方bundles,始终使用参数,因为他们要让存于容器中的服务具备更强的“可配置性”。当然,你可能并不需要参数带来的灵活性。

数组参数 

参数可以包含数组,参见数组参数(Array Parameters)

引用(注入)服务 

至此,原来的app.mailer服务是简单的:它在构造器中只有一个参数,因此很容易配置。你可以预见到,在你创建一个需要依赖一个或多个容器中的其他服务时,容器的真正威力开始体现出来。

例如,你有一个新的服务,NewsletterManager,它帮助你管理和发送邮件信息到地址集。app.mailer服务已经可以发邮件了,因此你可以把它用在NewsletterManager中来负责信息传送的部分。这个类看上去像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Acme/HelloBundle/Newsletter/NewsletterManager.php
namespace Acme\HelloBundle\Newsletter;
 
use Acme\HelloBundle\Mailer;
 
class NewsletterManager
{
    protected $mailer;
 
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
 
    // ...
}

如果不使用服务容器,你可以在controller中很容易地创建一个NewsletterManager:

1
2
3
4
5
6
7
8
9
10
use Acme\HelloBundle\Newsletter\NewsletterManager;
 
// ...
 
public function sendNewsletterAction()
{
    $mailer = $this->get('my_mailer');
    $newsletter = new NewsletterManager($mailer);
    // ...
}

这样去实现是可以的,但当你以后要对NewsletterManager类增加第二或第三个构造器参数时怎么办?如果你决定重构代码并且重命名这个类时怎么办?这两种情况,你都需要找到每一个NewsletterManager类被实例化的地方,然后手动个性它。毫无疑问,服务容器提供了一个更加吸引人的处理方式:

1
2
3
4
5
6
7
8
# app/config/services.yml
services:
    app.mailer:
        # ...

    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        arguments: ['@app.mailer']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- app/config/services.xml -->
<?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="app.mailer">
        <!-- ... -->
        </service>
 
        <service id="app.newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
            <argument type="service" id="app.mailer"/>
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
10
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
$container->setDefinition('app.mailer', ...);
 
$container->setDefinition('app.newsletter_manager', new Definition(
    'AppBundle\Newsletter\NewsletterManager',
    array(new Reference('app.mailer'))
));

在YAML中,特殊的@app.mailer语法,告诉容器去寻找一个名为app.mailer的服务,然后把这个对象传给NewsletterManager的构造器参数。本例中,指定的app.mailer服务是确实存在的。如果它不存在,则异常会抛出。不过你可以标记依赖可选 – 这个话题将在下一小节中讨论。

(对服务的)引用是个强大的工具,它允许你创建独立的服务,却拥有准确定义的依赖关系。在这个例子中, app.newsletter_manager 服务为了实现功能,需要依赖 app.mailer 服务。当你在服务容器中定义了这个依赖时,容器托管了对这个类进行实例化的全部工作。

可选的依赖:Setter注入 

将依赖对象注入到构造器中是一个办法,这可确保依赖可以利用(否则构造函数无法执行)。但是对一个类来说,如果它有一个可选的依赖,那么“setter注入”是一个更好的方案。这意味着用一个类方法来注入依赖,而不是构造器。这个类看上去可能是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace AppBundle\Newsletter;
 
use AppBundle\Mailer;
 
class NewsletterManager
{
    protected $mailer;
 
    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
 
    // ...
}

服务定义需要对setter注入做出相应调整:

1
2
3
4
5
6
7
8
9
# app/config/services.yml
services:
    app.mailer:
        # ...

    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        calls:
            - [setMailer, ['@app.mailer']]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- app/config/services.xml -->
<?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="app.mailer">
        <!-- ... -->
        </service>
 
        <service id="app.newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
            <call method="setMailer">
                <argument type="service" id="app.mailer" />
            </call>
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
10
11
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
$container->setDefinition('app.mailer', ...);
 
$container->setDefinition('app.newsletter_manager', new Definition(
    'AppBundle\Newsletter\NewsletterManager'
))->addMethodCall('setMailer', array(
    new Reference('app.mailer'),
));

本节所实现的过程被称为“构造器注入”和“setter注入”。此外Symfony的容器体系还支持属性注入(property injection)。

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

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