利用父服务来管理常规依赖

3.4 版本
维护中的版本

当你向程序中添加更多功能时,就有可能产生一些相关联的类,这些类共享着某些相同的依赖。例如你有一个Newsletter Manager,它使用setter注入来获取依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class NewsletterManager
{
    protected $mailer;
    protected $emailFormatter;
 
    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
 
    public function setEmailFormatter(EmailFormatter $emailFormatter)
    {
        $this->emailFormatter = $emailFormatter;
    }
 
    // ...
}

然后是一个Greeting Card类,与上面的代码共享了相同的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class GreetingCardManager
{
    protected $mailer;
    protected $emailFormatter;
 
    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
 
    public function setEmailFormatter(EmailFormatter $emailFormatter)
    {
        $this->emailFormatter = $emailFormatter;
    }
 
    // ...
}

这两个类的服务配置,可能是下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
services:
    my_mailer:
        # ...

    my_email_formatter:
        # ...

    newsletter_manager:
        class: NewsletterManager
        calls:
            - [setMailer, ['@my_mailer']]
            - [setEmailFormatter, ['@my_email_formatter']]

    greeting_card_manager:
        class: GreetingCardManager
        calls:
            - [setMailer, ['@my_mailer']]
            - [setEmailFormatter, ['@my_email_formatter']]
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
<?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="my_mailer">
            <!-- ... -->
        </service>
 
        <service id="my_email_formatter">
            <!-- ... -->
        </service>
 
        <service id="newsletter_manager" class="NewsletterManager">
            <call method="setMailer">
                <argument type="service" id="my_mailer" />
            </call>
            <call method="setEmailFormatter">
                <argument type="service" id="my_email_formatter" />
            </call>
        </service>
 
        <service id="greeting_card_manager" class="GreetingCardManager">
            <call method="setMailer">
                <argument type="service" id="my_mailer" />
            </call>
 
            <call method="setEmailFormatter">
                <argument type="service" id="my_email_formatter" />
            </call>
        </service>
    </services>
</container>
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
use Symfony\Component\DependencyInjection\Reference;
 
// ...
$container->register('my_mailer', ...);
$container->register('my_email_formatter', ...);
 
$container
    ->register('newsletter_manager', 'NewsletterManager')
    ->addMethodCall('setMailer', array(
        new Reference('my_mailer'),
    ))
    ->addMethodCall('setEmailFormatter', array(
        new Reference('my_email_formatter'),
    ))
;
 
$container
    ->register('greeting_card_manager', 'GreetingCardManager')
    ->addMethodCall('setMailer', array(
        new Reference('my_mailer'),
    ))
    ->addMethodCall('setEmailFormatter', array(
        new Reference('my_email_formatter'),
    ))
;

无论是类的代码还是配置代码,都存在着大量的重复内容。这意味着当你做出修改时,比如,EmailFormattermailer要通过构造器来注入,你需要更新配置文件中的两个地方。如同你在setter方法中做出修改时,你要在两个类中进行操作。对于关联类中的共性方法,一个典型处理方式是,把这些共性内容剥离到超类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class MailManager
{
    protected $mailer;
    protected $emailFormatter;
 
    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
 
    public function setEmailFormatter(EmailFormatter $emailFormatter)
    {
        $this->emailFormatter = $emailFormatter;
    }
 
    // ...
}

这样一来,NewsletterManagerGrettingCardManager可以继承超类:

1
2
3
4
class NewsletterManager extends MailManager
{
    // ...
}

以及:

1
2
3
4
class GreetingCardManager extends MailManager
{
    // ...
}

类似的,Symfony服务容器也支持在配置文件中实现“服务继承”,这样你就可以减少冗余代码,通过指定一个父服务来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ...
services:
    # ...
    mail_manager:
        abstract:  true
        calls:
            - [setMailer, ['@my_mailer']]
            - [setEmailFormatter, ['@my_email_formatter']]

    newsletter_manager:
        class:  NewsletterManager
        parent: mail_manager

    greeting_card_manager:
        class:  GreetingCardManager
        parent: mail_manager
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
<?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="mail_manager" abstract="true">
            <call method="setMailer">
                <argument type="service" id="my_mailer" />
            </call>
 
            <call method="setEmailFormatter">
                <argument type="service" id="my_email_formatter" />
            </call>
        </service>
 
        <service
            id="newsletter_manager"
            class="NewsletterManager"
            parent="mail_manager" />
 
        <service
            id="greeting_card_manager"
            class="GreetingCardManager"
            parent="mail_manager" />
    </services>
</container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;
 
// ...
$mailManager = new Definition();
$mailManager
    ->setAbstract(true);
    ->addMethodCall('setMailer', array(
        new Reference('my_mailer'),
    ))
    ->addMethodCall('setEmailFormatter', array(
        new Reference('my_email_formatter'),
    ))
;
$container->setDefinition('mail_manager', $mailManager);
 
$newsletterManager = new DefinitionDecorator('mail_manager');
$newsletterManager->setClass('NewsletterManager');
$container->setDefinition('newsletter_manager', $newsletterManager);
 
$greetingCardManager = new DefinitionDecorator('mail_manager');
$greetingCardManager->setClass('GreetingCardManager');
$container->setDefinition('greeting_card_manager', $greetingCardManager);

根据上下文,拥有父服务即暗示子服务必须应用父服务的参数和方法等。特别是父服务中定义的setter方法,在子服务被实例化的时候,将会被调用。

如果你去在上述配置代码中去掉parent键,相关服务仍然会被实例化,并且它们仍然继承了MailerManager类。区别在于,忽略parent配置键,意味着mail_manager服务中定义的方法,在子服务实例化的时候,将不再被调用。

scopeabstracttag属性应始终从子类中剥离出来。

父服务是“抽象”的,表示它不会被从服务容器中直接取出,也不能传给另一个服务。抽象的存在,仅是作为“模板”供其他服务取用。这就是为什么在配置代码中没有class键的原因,否则的话,会报一个“需要非抽象服务”的异常。

为了使父服务能被解析,ContainerBuilder必须先被编译。参阅本章前面的编译服务容器小节。

在上面例子中,一些类共享了相同的配置信息,并在PHP中继承同一个父类。这绝非“必须行为”。你可以只把服务定义中的通用部分,分离到父服务中,但却不在PHP中继承任何父类。

覆写parent dependencies 

有时候,你需要覆写的仅仅是一个“被当做依赖传入子服务”的类。幸运的是,通过在配置文件中添加对该子服务的“方法调用”,于父服务中设定好的依赖可以被复写。因此,若你需要传递一个不同的依赖给NewsletterManager类,按下述方法配置服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ...
services:
    # ...
    my_alternative_mailer:
        # ...

    mail_manager:
        abstract: true
        calls:
            - [setMailer, ['@my_mailer']]
            - [setEmailFormatter, ['@my_email_formatter']]

    newsletter_manager:
        class:  NewsletterManager
        parent: mail_manager
        calls:
            - [setMailer, ['@my_alternative_mailer']]

    greeting_card_manager:
        class:  GreetingCardManager
        parent: mail_manager
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
<?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="my_alternative_mailer">
            <!-- ... -->
        </service>
 
        <service id="mail_manager" abstract="true">
            <call method="setMailer">
                <argument type="service" id="my_mailer" />
            </call>
 
            <call method="setEmailFormatter">
                <argument type="service" id="my_email_formatter" />
            </call>
        </service>
 
        <service
            id="newsletter_manager"
            class="NewsletterManager"
            parent="mail_manager">
 
            <call method="setMailer">
                <argument type="service" id="my_alternative_mailer" />
            </call>
        </service>
 
        <service
            id="greeting_card_manager"
            class="GreetingCardManager"
            parent="mail_manager" />
    </services>
</container>
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
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;
 
// ...
$container->setDefinition('my_alternative_mailer', ...);
 
$mailManager = new Definition();
$mailManager
    ->setAbstract(true)
    ->addMethodCall('setMailer', array(
        new Reference('my_mailer'),
    ))
    ->addMethodCall('setEmailFormatter', array(
        new Reference('my_email_formatter'),
    ))
;
$container->setDefinition('mail_manager', $mailManager);
 
$newsletterManager = new DefinitionDecorator('mail_manager');
$newsletterManager->setClass('NewsletterManager');
    ->addMethodCall('setMailer', array(
        new Reference('my_alternative_mailer'),
    ))
;
$container->setDefinition('newsletter_manager', $newsletterManager);
 
$greetingCardManager = new DefinitionDecorator('mail_manager');
$greetingCardManager->setClass('GreetingCardManager');
$container->setDefinition('greeting_card_manager', $greetingCardManager);

GreetingCardManager将收到和以前相同的依赖,但是NewsletterManager将被传入my_alternative_mailer而不是my_mailer service

你不能覆写方法调用。当你在子服务中定义一个新method call时,它将被添加到现有的“已配置方法集”中。这意味着,当setter覆写了当前属性时,它能够工作得很好——然而在setter把该属性附加到已经存在的数据时(例如addFilters()方法),它就难以按预期工作了。此时,唯一的解决方案就是,不去继承父服务而是按常规情形来配置服务(如同之前你并不知道有“父服务”概念)。

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

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