symfony2学习笔记(6)——服务容器

一、什么是服务容器?

简而言之,一个服务就是一个日志类或者邮件类,通过配置文件把这些类独立出来让任何地方都可以调用到。


二、为什么使用服务容器?


  1. 每个服务都会应用于你整个应用程序,无论何时你需要他们的时候,他们都能提供。当你需要的时候你可以很容易的访问每个服务和使用它们的功能。

  2. 每个服务也必须能很容易的测试和配置,因为它们是从你应用程序的其他的功能中分离了出来的。

  3. 比如,假设我们有一个简单的PHP类分发Email信息。 没有服务容器,我们必须在使用它的时候手动的创建这个对象:

use Acme\HelloBundle\Mailer;
$mailer = new Mailer('sendmail');
$mailer->send('ryan@foobar.net',...);

  这看上去很容易,假设Mailer类允许我们配置用于分发邮件信息的方法(sendmail,smtp等)。但是如果我们想在别的地方使用mailer服务? 我们又不想重复的在每一个使用Mailer的地方配置它。如果我们需要改变邮件发送的地址怎么办?我们需要找到每一个Mailer配置的然后修改代码。


三、如何创建和使用服务容器呢?


(1)创建/配置服务

(2)服务参数

(3)数组参数

(4)导入其它容器配置资源

(5)引用(注入)服务

(6)可选依赖:setter注入

(7)设置可选引用

(8)Symfony核心和第三方bundle服务

(9)高级容器配置


(1)创建/配置服务

  这些工作是通过配置文件来实现的,配置文件你可以设置成YAML,XML或者PHP格式均可。本章只介绍yml格式,如需其他格式,可去官网查看。

#app/config/config.yml
services:
    my_mailer:
        class:     Acme\HelloBundle\Mailer
        arguments: [sendmail]

  当Symfony2 初始化时,它会默认根据应用程序配置(app/config/config.yml)创建服务容器。真正的服务容器配置文件是AppKernel::registerContainerConfiguration()方法加载的一个环境特定配置文件,config_dev.yml 用于开发阶段,config_prod.yml用于运营阶段。

  Symfony2配置启动后,AcmeHelloBundleMailer的对象实例就能够通过服务容器使用了。服务容器可以在任何传统的Symfony2的controller中使用,通过简写方法 get()调用。

class HelloController extends Controller
{
   //...
   public function sendEmailAction()
   {
      //...
      $mailer = $this->get('my_mailer');
      $mailer->send('ryan@foobar.net',...);
   }
}

  当我们从服务容器中获取Mailer实例时,使用my_mailer. 容器会创建这个对象并返回它。有另外一个好处,服务直到用到它时才会创建。这样能够节省内存提高程序效率。

(2)服务参数:

  通过服务容器创建一个新服务很简单,参数的设置会使定义更加有组织性和有适应性。

#app/config/config.yml
parameters:
   my_mailer.class:     Acme\HelloBundle\Mailer
   my_mailer.transport: sendmail
              
services:
   my_mailer:
      class:     %my_mailer.class%
      arguments: [%my_mailer.transport%]

  上面配置文件中,%% 是参数定义方式,当容器创建完成后就会查找参数定义,如果%参数或者变量作为字符串一部分时,需要添加另一个%进行转换

  参数的目的就是把信息传入服务,当然,不定义任何参数也是没有问题的。

  定义参数会具有某些优势:

    在一个参数键parameters中分离和组织服务的所有可选项。

    参数值可以用于多个服务的定义。

    在一个bundle中创建一个服务,使用参数可以很容易的在你的应用程序中进行自定义化服务。

  当然,用不用参数完全取决于你的决定。高质量的第三方bundles总是使用参数,当他们把服务放入容器中使其更具可配置性。当然,在你的应用程序中你可能不需要这样的配置性。

(3)数组参数

  参数不一定都是普通字符串,也有可能是数组。如果是写在XML格式的配置文件中,那么你需要为数组参数定义type="collection" 属性。

# app/config/config.yml
parameters:
    my_mailer.gateways:
        - mail1
        - mail2
        - mail3
    my_multilang.language_fallback:
        en:
            - en
            - fr
        fr:
            - fr
            - en

XML格式:

<!-- app/config/config.xml -->
<parameters>
    <parameter key="my_mailer.gateways" type="collection">
        <parameter>mail1</parameter>
        <parameter>mail2</parameter>
        <parameter>mail3</parameter>
    </parameter>
    <parameter key="my_multilang.language_fallback" type="collection">
        <parameter key="en" type="collection">
            <parameter>en</parameter>
            <parameter>fr</parameter>
        </parameter>
        <parameter key="fr" type="collection">
            <parameter>fr</parameter>
            <parameter>en</parameter>
        </parameter>
    </parameter>
</parameters>

(4)导入其它容器配置资源:

  Symfony2 非常灵活,它的配置可以放到任何地方,比如数据库更或者外部的一个webservice。

  服务容器默认情况下用一个单一的配置资源创建(app/config/config.yml)。其它所有的服务配置必须从这个文件中一次或者多次导入。这包括Symfony2核心配置和第三方bundle配置。这给你的应用程序在服务上有了相对的灵活性。

  扩展的服务配置可以通过两种方式导入:第一,我们最常用的是 imports 命令。接下来我们会介绍第二种方法,它是更灵活并且是导入第三方bundles中的服务配置的首选。

1. 通过imports导入配置

  到目前为止,我们是把my_mailer服务的配置直接定义到了应用程序配置文件(app/config/config.yml)中了。

  当然,因为Mailer本身就在AcmeHelloBundle中,其实把my_mailer的容器定义放到bundle中也可以。

首先,我们把my_mailer的容器定义移到一个新的容器资源文件中把它放到AcmeHelloBundle之外。如果Resources或者Resources/config 目录不存在,我们创建它。

# src/Acme/HelloBundle/Resources/config/services.yml  #注意这里文件路径发生了变化
parameters:
    my_mailer.class:      Acme\HelloBundle\Mailer  
    my_mailer.transport:  sendmail
              
services:
    my_mailer:
        class:        %my_mailer.class%
        arguments:    [%my_mailer.transport%]

  当然,服务容器不知道我们的资源文件新位置。我们可以利用imports 键在应用程序的配置文件中很容易的导入它们。

# app/config/config.yml  #注意这里的位置
imports:
    - { resource: @AcmeHelloBundle/Resources/config/services.yml }

  imports命令允许你的应用程序从其他地方获取服务容器的配置资源(一般是从一些bundle中)。resource的位置,对于文件资源,是绝对路径。@AcmeHello语法决定了AcmeHelloBundle 的路径。这使得你指定资源路径的时候不用担心以后移动AcmeHelloBundle到别的目录的问题。

2. 使用容器扩展导入配置

在使用Symfony2 开发过程中,你将经常用到imports命令来从你创建的bundle中导入容器配置。而第三方的bundle容器配置,包括Symfony2的核心服务在内,通常使用另外一种方法,它更灵活更易于配置。


它是如何工作的呢?

  一个bundle使用一个或者多个配置资源文件(通常是XML)来指定bundle所需要的参数和服务。然而,我们不直接在配置文件中使用imports命令导入它们,而是仅仅在bundle中调用一个服务容器扩展来为我们做同样的工作。

一个服务容器扩展bundle的作者创建的是一个PHP类,它主要完成两件事情:

  为该bundle配置服务导入需要的所有的服务容器资源。

  提供语法上简介配置,让bundle能够直接被配置,而不需要再与bundle的服务容器配置参数交互。

  换句话说,一个服务容器扩展会帮你配置好它的bundle所需的服务。

让我们看看FrameworkBundle是如何做的?

  FrameworkBundle是Symfony2框架bundle,下面的代码显示了在你的应用程序配置中调用FrameworkBundle中的服务容器扩展。


# app/config/config.yml
framework:
    secret:          xxxxxxxxxx
    charset:         UTF-8
    form:            true
    csrf_protection: true
    router:        { resource: "%kernel.root_dir%/config/routing.yml" }
    # ...

当这个配置被解析时,容器会查找一个可以处理framework配置命令的扩展。这个扩展在FrameworkBundle中,它会被调用来为FrameworkBundle加载服务配置。如果你从你的配置文件中完全去掉framework键,Symfony 核心服务将不会被加载。这完全是由你控制的。

  当然,你可以做更多,而不只是激活FrameworkBundle的服务容器扩展。每个扩展都允许你很容易的个性化bundle,而不用关系其内部服务是怎么定义的。

  比如你可以个性化charset,error_handler,csrf_protection,router等配置。实际上,FrameworkBundle使用的这里指定的项目来配置服务于它自身的服务配置。bundle负责为服务容器创建所有需要的parameters和services,同时依然允许大量的配置可以被个性化。作为一个额外的好处,大部分服务容器扩展能够执行校验,通知你那些选项丢失或者数据类型不正确。当安装或者配置一个bundle时应该看看bundle的说明,了解一下如何安装和配置它需要的服务。

  注意:服务容器天生能够识别parameters,services 和imports命令,其它的命令则需要服务容器扩展来处理。

(5)引用(注入)服务:

到目前为止,我们创建的my_mailer服务非常简单:它仅仅在它的构造函数中接受一个参数,非常容易配置。只有当我们需要创建一个服务而它又依赖于一个或者多个其它服务时,我们才能看到服务容器的真正威力。

  让我们来看个例子:

  假设我们有个新服务,NewsletterManager,它用于管理准备和分发一个邮件信息到一组地址。当然,my_mailer已经能够发送邮件信息了,所以我们将在NewsletterManager内部使用它。

  假设它的类内容如下:

namespace Acme\HelloBundle\Newsletter;
              
use Acme\HelloBundle\Mailer;
              
class NewsletterManager
{
    protected $mailer;
              
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
              
    // ...
}

上面的例子代码中没有使用服务容器,我们可以很容易的在controller内创建一个新的NewsletterManager实例。

public function sendNewsletterAction()
{
    $mailer = $this->get('my_mailer');
    $newsletter = new Acme\HelloBundle\Newsletter\NewsletterManager($mailer);
    // ...
}

这种方式是好的,但是如果我们决定NewsletterManager类需要第二个或者第三个构造函数参数呢?

  我们可以重写代码并重新命名该类来实现,可是我们需要找到所有的使用NewsletterManager的地方修改它。这是很痛苦的事情,这时候服务容器就成了一个很诱人的选择。

# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
    # ...
    newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager
              
services:
    my_mailer:
        # ...
    newsletter_manager:
        class:     %newsletter_manager.class%
        arguments: [@my_mailer]

  在YAML配置文件中,@my_mailer告诉容器去查找一个名叫my_mailer的服务对象并把它传递给NewsletterManager的构造函数。在这种情况下,指定的服务my_mailer必须存在。如果不存在,将会抛出异常。你可以把你的依赖标记为可选,接下来说明。

(6)可选依赖:setter注入

  通过构造函数参数方式注入一个依赖在依赖已经存在并可用的情况下是一个完美的方式。但是当一个类的依赖是可选的时候,setter注入就成了更好的选择。

  或者说当我们在某个服务加载时就要执行某个方法来进行某个操作的时候也是极好的选择。

namespace Acme\HelloBundle\Newsletter;
              
use Acme\HelloBundle\Mailer;
              
class NewsletterManager
{
    protected $mailer;
              
    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
              
    // ...
}

  相应的配置上只需要一点改动:

# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
    # ...
    newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager
              
services:
    my_mailer:
        # ...
    newsletter_manager:
        class:     %newsletter_manager.class%
        calls:
            - [ setMailer, [ @my_mailer ] ]

(7)设置可选引用:

  有时候,你的应用可能有一个可选的依赖,这就意味着这个依赖对于你的服务运行不是必须的。上面例子中,my_mailer服务必须存在,所以没有它时会抛出异常。我们来修改newsletter_manager服务定义,让这个依赖变为可选依赖。这样当它存在是容器会注入它,如果不存在时,什么也不做。

# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
    # ...
              
services:
    newsletter_manager:
        class:     %newsletter_manager.class%
        arguments: [@?my_mailer]

  在YAML配置文件中,@? 语法标示告诉服务容器该依赖是可选的。当然,NewsletterManager类也需要相应的修改一下构造函数:

public function __construct(Mailer $mailer = null)
{
    // ...
}

(8)Symfony核心和第三方bundle服务:

从Symfony2和所有第三方bundles的配置都通过容器获取他们的服务, 你可以很容易的访问他们或者在你自己的服务中使用他们。为了保持简洁,Symfoy2模式不需要controller也定义成服务。而是Symfony2把整个服务注入到所有的Controller中。比如,处理在用户Session中存在信息时,Symfony2 提供了一个session服务,你可以在一个标准controller中直接调用:

public function indexAction($bar)
{
    $session = $this->get('session');
    $session->set('foo', $bar);
              
    // ...
}

在Symfony2中,你将经常使用Symfoy或第三方bundles提供的服务来执行任务,比如渲染模板的templating, 发送邮件的mailer访问请求信息的request等。


我们可以进一步的在我们自己创建的服务中调用这些服务。让我们修改NewsletterManager使用真正的Symfony2 mailer服务。同时我们还为其传入模板引擎,让它通过一个模板生成邮件内容。

namespace Acme\HelloBundle\Newsletter;
              
use Symfony\Component\Templating\EngineInterface;
              
class NewsletterManager
{
    protected $mailer;
              
    protected $templating;
              
    public function __construct(Swift_Mailer $mailer, EngineInterface $templating)
    {
        $this->mailer = $mailer;
        $this->templating = $templating;
    }
              
    // ...
}

配置服务容器:

services:
    newsletter_manager:
        class:     %newsletter_manager.class%
        arguments: [@mailer, @templating]

(9)高级容器配置:

  到此我们看到,在容器中定义一个服务非常简单,通常包含一个服务的配置键和一些参数。然而,容器还有许多其它的可用工具帮助标志(tag)服务为特定的功能。 以创建更复杂的服务,在容器建立后执行操作。

  设置服务为public/private

  在定义服务的时候,你通常想在应用程序范围内访问它们,这些服务叫做public服务。比如doctrine服务在使用DoctrineBundle注册时就是一个公共服务,你可以按照如下方式访问:

$doctrine = $container->get('doctrine');

  然而,在某些情况下你不想一个服务变为公共的。这种情况通常出现在你创建某个服务只是为另外一个服务作为输入参数时出现。private 私有服务,这些服务在调用的时候只能在参数行内通过 new PrivateFooBar()形式引用。

  简单的说:一个服务当你不想它被你的代码直接访问时,它就是私有的了。

  配置形式如下:

services:
   foo:
     class: Acme\HelloBundle\Foo
     public: false

  这时候你就不能再进行如此操作了:


$container->get('foo');

  注意:服务默认情况下全部是公共的。如果一个服务被配置为private了,但是你还想引用它,那么你需要给它定义别名。


(10)服务的别名

  当使用核心或者第三方bundles提供的服务时,你可能想用更加方便快捷的形式调用某些服务。你可以通过给他们定义别名来实现,甚至给私有服务定义别名。


services:
   foo:
     class: Acme\HelloBundle\Foo
   bar:
     alias: foo

  这时你可以通过别名来直接调用以前的私有服务了,比如:

$container->get('bar'); // 将返回以前私有服务 foo

(11)要求必备文件:

  有些情况下,你需要在加载服务前包含其它文件,这时你可以使用file命令实现:

services:
   foo:
     class: Acme\HelloBundle\FooBar
     file: %kernel.root_dir%/src/path/to/file/foo.php

  注意:Symfony将在内部调用PHP函数require_once,这就意味着你的文件将每个请求都会被包括一次。

(12)服务标签(Tags):

  就像你在网络上发表博客可以设置标签“Symfony”或者“PHP”等一样,你在容器中配置的服务也可以被贴上标签。一个标签暗示这个服务是被用于特殊目的的。比如:

services:
    foo.twig.extension:
        class: Acme\HelloBundle\Extension\FooExtension
        tags:
            -  { name: twig.extension }

  这里的twig.extension 标签就是一个专用标签,是TwigBundle在配置时使用的。通过给服务标注这个twig.extension标签,bundle就会知道foo.twig.extension 服务应该被注册为一个Twig的扩展。换句话说,Twig会查找所有标记为twig.extension的服务并自动把它们注册为扩展。

(13)下面列出了Symfony2核心bundles的可用的标签:

assetic.filter

assetic.templating.php

data_collector

form.field_factory.guesser

kernel.cache_warmer

kernel.event_listener

monolog.logger

routing.loader

security.listener.factory

security.voter

templating.helper

twig.extension

translation.loader

validator.constraint_validator


参考URL:http://symfony.com/doc/current/book/service_container.html


四、相关文章


(1)Symfony 一个service里 调用其他service

by 雪洁 2015-03-10 07:15:22 4085 views
我来说几句

相关文章