Symfony2Book15:服务容器

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介:

现代的PHP应用程序是完全面向对象的。一个对象也许方便地发送电子邮件消息,而另一个对象也许让持久化信息到数据库。在你的应用程序中,你也许创建一个对象来管理你的产品库存、或者从第三方API中处理数据。问题的关键是现代应用程序可以做太多的事了,是到了将许多处理每个任务的对象组织起来的时候了。

在本章,我们讨论Symfony2中一个特殊的PHP对象,它可以帮助你实例化、组织和恢复你应用程序中的许多对象。这个对象被称为服务容器,它让你规范和集中管理应用程序中构建对象的方法。该容器让你生活更为轻松,它超级快,并且它强调能促进可重用和松耦合编码的体系结构。而且,由于所有核心Symfony2类都使用容器,你将学会如何扩展,配置和使用Symfony2的对象。在很大程度上,服务容器是Symfony2高速、可扩展的最大贡献者。

最后配置和使用服务容器是容易的。在本章结束,你将舒适地通过容器创建你自己的对象,并从任意第三方Bundle中自定义对象。你将开始编写更多可重用、可测试和松耦合的代码。这很简单,因为服务容器使编写好代码变得如此的容易。

什么是服务

简单来说,服务就是任何执行某些“全局”任务的PHP对象。它是一个在计算机学科里太过通用的名字,它用于描述为某一目的(如发送邮件)而创建的对象。无论何时,每个服务都用于整个应用程序中,以便它随时提供你所需的特定功能。你也无须特殊去做什么来创建一个服务:只须简单地编写一个完成某一特殊任务的PHP类。恭喜你,你刚刚创建了一个服务!

作为一个规则,一个PHP对象在你应用程序全局范围内使用的话,它就是一个服务。单个的Mailer服务就是在全局范围内发送邮件消息。而它所发送的Message对象就不是服务。同样的,Product对象不是服务,但将Product对象持久化到数据库的对象是一个服务。

所以这有什么大不了的呢?考虑“服务”的好处在于你开始将你应用程序的功能划分为一系列的服务。因为每个服务只做一项工作,所以你可以很容易地访问每个服务,并在你需要时使用它的功能。每个服务也更容易测试和配置,因为它是从你应用程序的其它功能中划分了来的。这种思路被称为面向服务的体系结构,它并不是Symfony2甚至PHP所独有的。围绕着一系列独立服务类来结构化应用程序是众所周知和值得信赖的最佳实践。这些技巧是几乎所有语言中成为一个优秀开发者的关键。

什么是服务容器

服务容器(或依赖注入容器)是个简单的PHP对象,它管理服务的实例(如对象)。例如,假设我们有一个发送邮件消息的简单PHP类。没有服务容器,我们必须在我们需要时手工创建对象:

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

这非常简单。虚构的Mailer类让我们配置用于发送邮件消息的方法(如:sendmail、smtp等)。但如果我们想在某处也使用邮件服务呢?我们一定不想每次当我们需要用Mailer对象时都重复邮件配置。如果我们需要在应用程序中将每个地方的传输都要从sendmail改成smtp呢?我们需要追踪每个创建Mailer服务的地方,并进行修改。

在容器中创建/配置服务

更好的做法是让服务容器去为你创建Mailer对象。为了能工作,我们必须教容器怎样去创建Mailer服务。这可以通过配置实现,它可以用YAML、XML或PHP格式指定:

 
  1. # app/config/config.yml 
  2. services: 
  3.     my_mailer: 
  4.         class:        Acme\HelloBundle\Mailer 
  5.         arguments:    [sendmail] 

当Symfony2初始化时就使用应用程序配置(缺省是app/config/config.yml)构建了服务容器。该文件直接被AppKernel::loadConfig()方法导入,该方法引导特定环境的配置文件(如:开发环境的config_dev.yml和生产环境的config_prod.yml)。

Acme\HelloBundle\Mailer对象的实例现在可以通过服务容器生成了。因为容器在任何传统的Symfony2控制器中都是可用的,所以我们可以很方便地访问新的my_mailer服务:

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

当我们要求来自容器的my_mailer服务时,容器构造该对象并将之返回。这是使用服务容器的另一个主要优势。换而言之,服务只在它被需要时构建。如果你定义一个服务,但从来没在请求中使用它,那么该服务将永远不会创建。这节省了内存并提高了你应用程序的速度。这也意味着定义了大量的服务,但很少或没有使用的话,服务将永远不会被使用和构建。

做为附带的奖励,Mailer服务只被创建一次,然后同一实例可以在你每次请求服务时返回。大多数时间里,你总是需要这样的行为(它更加灵活强大),但我们稍后学习如何配置拥有多个实例的服务。

服务参数

通过容器来创建新服务(如对象)是非常直接的。参数可以让服务更具组织性和灵活性:

 
  1. # app/config/config.yml 
  2. parameters: 
  3.     my_mailer.class:      Acme\HelloBundle\Mailer 
  4.     my_mailer.transport:  sendmail 
  5.  
  6. services: 
  7.     my_mailer: 
  8.         class:        %my_mailer.class% 
  9.         arguments:    [%my_mailer.transport%] 

最终结果与前面的相同,只是在定义服务是有着稍许不同。通过使用百分号(%)包围着的my_mailer.class和my_mailer.transport字符串,容器知道使用这些名字来查找参数。当容器被构建时,它查询每个参数的值,并在服务定义中使用它。

使用参数的目的是将信息送给服务。当然在定义服务时不使用任何参数也不会发生错误。然而,使用参数有几点好处:

  • 在单个参数关键词下划分和组织所有的服务“选项”;
  • 参数值可以在多个服务定义中使用;
  • 在Bundle中创建服务时(我们将稍后进行展示),使用参数可以让你应用程序中的服务更易定制。

使不使用参数由你决定。高质量的第三方Bundle总是使用参数,因为它们需要被保存在容器中服务的更多配置。然而为了你应用程序中的服务,你也许并不需要参数的灵活性。

导入其它的容器配置资源

在本节,我们指出服务配置文件被当作资源。为了突出这一事实,大多数的配置资源都是文件(如:YAML、XML和PHP),Symfony2非常灵活,配置可以从任何地方(如数据库,甚至可以是外部的Web服务)导入。

服务容器使用单个配置资源(缺省是app/config/config.ym)。所有其它服务配置(包括核心Symfony2和第三方Bundle配置)必须从该文件内部通过一种或多种方式导。这给你应用程序中的服务以绝对的灵活性。

外部服务配置可以通过两个不同的方式导入。首先,我们将讨论应用程序中最通过的方式:import指令。在下节中,我们将介绍第二种方式,从第三方Bundle导入服务配置是一种灵活的首选方式。

用imports导入配置

到目前为止,我们已经直接在应用程序配置文件(如:app/config/config.yml)中定义了我们的my_mailer服务容器。当然,因为Mailer类自身是在AcmeHelloBundle中的,因此将my_mailer容器放入Bundle中更有意义。

首先将my_mailer容器定义移入AcmeHelloBundle中新容器资源文件。如果Resources或Resources/config指令不存在,创建它们。

 
  1. # src/Acme/HelloBundle/Resources/config/services.yml 
  2. parameters: 
  3.     my_mailer.class:      Acme\HelloBundle\Mailer 
  4.     my_mailer.transport:  sendmail 
  5.  
  6. services: 
  7.     my_mailer: 
  8.         class:        %my_mailer.class% 
  9.         arguments:    [%my_mailer.transport%] 

定义自身没有改变,改变的只是它的位置。当然服务容器并不知道新资源文件。幸运地是,我们可以使用imports关键词,很方便地在应用程序配置中导入资源文件。

 
  1. # app/config/config.yml 
  2. imports: 
  3.     hello_bundle: 
  4.         resource: @AcmeHelloBundle/Resources/config/services.yml 

imports指令让你的应用程序从其它任意地方(通常来自Bundle)包含服务容器配置资源。对于文件,资源位置是资源文件的绝对路径。特定的@AcmeHello语法解析指令 AcmeHelloBundle 的Bundle路径。这可以帮助你指定资源的路径,而无须担心以后你是否将 AcmeHelloBundle移到不同的目录。

通过容器扩展导入配置

用Symfony2开发,你最常使用的imports指令是从你应用程序中已创的特定Bundle中导入容器配置。包含在Symfony2核心服务中的第三方Bundle容器配置,通常使用另一种方法导入,它可方便灵活地在你应用程序中进行配置。

它是如何工作的话。在内部,正如我们所见Bundle定义的服务是非常多的。换句话说,Bundle使用一个或更多的配置资源文件(通常是XML)来为Bundle指定参数。然而,你可以简单在为你工作的Bundle中调用服务容器扩展,用以代替从你应用程序配置中使用imports指令来直接导入每个资源。服务容器扩展是一个由Bundle作者创建PHP类,它主要完成两项工作:

  • 为Bundle导入所有服务容器资源需要配置的服务
  • 提供直接语义配置,以便无须与Bundle的服务容器配置参数交互,就可以配置Bundle。

换句话说,服务容器扩展以你的名义为Bundle配置服务。正如我们立即看到的那样,扩展为配置Bundle提供了一个智能、高层次的接口。

为FrameworkBundle(核心Symfony2框架Bundle)为例。你应用配置的下列代码在FrameworkBundle中调用服务容器扩展:

 
  1. # app/config/config.yml 
  2. framework: 
  3.     secret:          xxxxxxxxxx 
  4.     charset:         UTF-8 
  5.     error_handler:   null 
  6.     form:            true 
  7.     csrf_protection: true 
  8.     router:        { resource: "%kernel.root_dir%/config/routing.yml" } 
  9.     # ... 

当配置被解析时,容器寻找可以处理框架配置指令的扩展。该扩展位于FrameworkBundle中,它被调用并且FrameworkBundle的服务配置被引导,这存在问题。如果你从你的应用程序配置文件的条目删除framework关键词,那么核心Symfony2服务将不会被引导。关键是你在控制:Symfony2框架没有包含任何魔法或者不受你控制地执行动作。

当然你可以比简单“激活”FrameworkBundle的服务控制器扩展做得更多。每个扩展让你可以很方便地自定义,而无须担心内部服务如何定义。

在这种情况下,扩展让你自定义charset、error_handler、csrf_protectiont、路由配置和更多。在内部,FramworkBundle使用这里指定的选项去定义和配置具体的服务。Bundle负责创建服务容器所有必须的参数和服务,并使大多数配置便于自定义。做为额外的好处,大多数服务容器扩展也足够智能去执行验证,以便通知你选项丢失或错误的数据类型。

当安装或配置Bundle时,请查阅Bundle的文档以了解Bundle服务应该如何安装和配置。对于核心Bundle,其选项在可参考指南中找到。

服务容器只承认参数、服务和imports指命。其它任何指令都被服务容器扩展处理。

引用(注入)服务

到目前为止,我们原始的my_mailer服务是简单的:它的构造函数只需要一个参数,十分容易配置。正如你所见,当你需要依赖容器中一个或更多服务时来创建服务时,容器真正的强大才得以体现。

让我们用一个例子开始。假设我们有一个新的服务,NewsletterManager,用来帮助管理准备和发送邮件消息到一个地址集。当然my_mailer服务已经准备好发送邮件消息,因为我们将在NewsletterManager中使用它去处理实际的消息发送。这个假想类也许看上去如下所示:

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

在没有使用服务容器时,我们可以在控制器内部很容易地创建一个新的NewsletterManager:

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

这种方式没错,但如果我们稍后决定NewsletterManager类需要第二个或第三个构造函数参数呢?如果我们决定重构我们的代码并重命名类呢?在这两种情况下,你将需要找到NewsletterManager每个实例化的地方并进行修改。当然,服务容器给了我们一个更合理的选项:

 
  1. # src/Acme/HelloBundle/Resources/config/services.yml 
  2. parameters: 
  3.     # ... 
  4.     newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager 
  5.  
  6. services: 
  7.     my_mailer: 
  8.         # ... 
  9.     newsletter_manager: 
  10.         class:     %newsletter_manager.class% 
  11.         arguments: [@my_mailer] 

在YAML格式中,特定的@my_mailer语法告诉容器查找名为my_mailer的服务,并将其对象发送给NewsletterManager的构造函数。然而,在这种情况下,特定服务my_mailer必须存在。如果它不存在的话,将会抛出一个异常。你可以将你的依赖关系作为可选,这将在下一节讨论。

使用引用是十分有用的工具,它让你创建独立的,有着定义良好依赖独立的服务类。在这个例子里,newsletter_manager服务需要my_mailer服务的功能。当你在服务容器中定义这个依赖时,容器负责对象实例化的所有工作。

可选依赖:Setter注入

以这种方式将依赖注入构造器是确保依赖可用的优秀方式。如果你有一个可选依赖,那么“Setter注入”也许是更好地选择。这意味着可以使用方法调用的方式来注入依赖,而非构造函数。该类看上去如下所示:

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

通过setter方法注入依赖只需要一点语法上的改变:

 
  1. # src/Acme/HelloBundle/Resources/config/services.yml 
  2. parameters: 
  3.     # ... 
  4.     newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager 
  5.  
  6. services: 
  7.     my_mailer: 
  8.         # ... 
  9.     newsletter_manager: 
  10.         class:     %newsletter_manager.class% 
  11.         calls: 
  12.             - [ setMailer, [ @my_mailer ] ] 

本节所介绍的方法被称为“构造函数注入”和“Setter注入”。Symfony2服务容器也支持“属性注入”。

将引用标为可选

有时,你的一个服务有一个可选的依赖,意思是你的服务不需要该依赖就能正常工作。在上面的例子里,my_mailer服务必须存在,否则将抛出异常。通过修改newsletter_manager服务的定义,你可以将该引用设为可选。容器在它存在时注入它,否则不做任何操作:

 
  1. # src/Acme/HelloBundle/Resources/config/services.yml 
  2. parameters: 
  3.     # ... 
  4.  
  5. services: 
  6.     newsletter_manager: 
  7.         class:     %newsletter_manager.class% 
  8.         arguments: [@?my_mailer] 

在YAML中,特定的@?语法告诉服务容器依赖是可选的。当然,NewsletterManager也必须被写成允许可选依赖:

 
  1. public function __construct(Mailer $mailer = null) 
  2.     // ... 

核心Symfony和第三方Bundle服务

因为Symfony2和所有第三方Bundle都是通过容器来配置和检索它们的服务,所以你可以很方便地访问它们,甚至在你的服务中使用它们。为了让一切简单,缺省情况下Symfony2并不要求控制器定义成服务。此外Symfony2注入整个服务容器到你的控制器。例如,要处理用户会话信息的保存,Symfony2提供会话服务。你可以在标准控制器中访问该服务,如下所示:

 
  1. public function indexAction($bar) 
  2.     $session = $this->get('session'); 
  3.     $session->set('foo', $bar); 
  4.  
  5.     // ... 

在Symfony2中,你经常会使用Symfony2核心或其它第三方Bundle提供的服务,去执行诸如渲染模板(templating)、发送邮件(mailer)或访问请求信息(request)。

我们可以更进一步,在你应用程序创建的服务中使用这些服务。让我们修改NewsletterManager以便使用真正的Symfony2邮件服务(代替假想的my_mailer)。让我们也将模板引擎服务发送到NewsletterManager,以便它可以通过模板生成邮件内容:

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

配置服务容器是容易的:

 
  1. services: 
  2.     newsletter_manager: 
  3.         class:     %newsletter_manager.class% 
  4.         arguments: [@mailer, @templating] 

newsletter_manager现在可以访问核心邮件和模板服务了。这是一种常见做法,利用框架中不同服务的强大来创建你应用程序的服务。

要确保swiftmailer条目在你应用程序配置中出现。正如我在通过容器扩展导入配置中提及的那样,swiftmailer关键词从SwiftmailerBundle(其中注册了邮件服务)中调用服务扩展。

高级容器配置

正如我们所见,定义控制器中的服务是方便的,通常涉及一个服务配置和若干参数。然而,容器另外有几个工具可以用来帮助标识特殊功能的服务,从而创建更为复杂的服务,并在容器构建后执行操作。

将服务标记成公共/私有

当定义服务时,你通常想你应用程序的代码可以访问这些定义。这些服务被称为公共服务。例如,当使用DoctrineBundle时通过容器注册的doctrine服务是个公共服务,你可以通过下列语句来访问它:

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

然而,也有不想服务成为公共服务的情况。这个通常定义只当做其它服务参数的服务时。

如果你使用一个私有服务作为多个其它服务的参数,那么它必须生成两个不同的私有服务实例(如:new PrivateFooBar())。

简单地说:当你不想在代码中直接访问A服务时,该服务就是私有服务。

这里是例子:
 
  1. services: 
  2.    foo: 
  3.      class: Acme\HelloBundle\Foo 
  4.      public: false 

现在服务是私有的,你不能调用:

 
  1. $container->get('foo'); 

然而,如果服务已经被标识为私有,你可以为它设置别名(看下面内容)去访问该服务(通过alias)。

服务缺省是公共的。

别名

当在应用程序中使用核心或第三方Bundle时,你可以想使用快捷方式去访问一些服务。你可以通过设置别名来实现这一点,此外,你甚至可以为非公共服务设置别名。

 
  1. services: 
  2.    foo: 
  3.      class: Acme\HelloBundle\Foo 
  4.    bar: 
  5.      alias: foo 

这意味着当直接使用容器时,你可以通过查找bar服务来访问foo服务。如下所示:

 
  1. $container->get('bar'); // 将返回foo服务

要求文件

可能存在这种情况:你需要在服务引导之前包含其它文件。要实现这一点,你可以使用file指令。

 
  1. services: 
  2.    foo: 
  3.      class: Acme\HelloBundle\Foo\Bar 
  4.      file: %kernel.root_dir%/src/path/to/file/foo.php 

注意Symfony2会内部调用PHP函数 require_once,它的意思是你的文件每个请求只包含一次。

标签(tags)

与在博客网站上给博文打标签(如:Symfony或PHP)一样,在你容器中配置服务也可以打标签。在服务容器中,标签意味着服务用于特定目标。下面是例子:

 
  1. services: 
  2.     foo.twig.extension: 
  3.         class: Acme\HelloBundle\Extension\FooExtension 
  4.         tags: 
  5.             -  { name: twig.extension } 

twig.extension标签是TwigBundle在配置时使用。通过给服务twig.extension标签,Bundle应该foo.twig.extension服务是通过Twig作为Twig扩展注册的。换句话说,Twig查找标签为twig.extension的所有服务,并自动将它们当作扩展注册。

然后,标签是告诉Symfony2或其它第三方Bundle你的服务应该通过Bundle以某些特定方式注册或使用。

下面是可用于Symfony2核心Bundle中的标签列表。每个标签对你的服务都有不同的效果,并且许多标签要求额外的参数(不仅仅是名字参数)

  • assetic.filter
  • assetic.templating.php
  • data_collector
  • form.field_factory.guesser
  • kernel.cache_warmer
  • kernel.listener
  • routing.loader
  • security.listener.factory
  • security.voter
  • templating.helper
  • twig.extension
  • translation.loader
  • validator.constraint_validator
  • zend.logger.writer

本文转自 firehare 51CTO博客,原文链接:http://blog.51cto.com/firehare/586264,如需转载请自行联系原作者
相关文章
|
21天前
|
关系型数据库 MySQL Nacos
【深入浅出Nacos原理及调优】「实战开发专题」采用Docker容器进行部署和搭建Nacos服务以及“坑点”
【深入浅出Nacos原理及调优】「实战开发专题」采用Docker容器进行部署和搭建Nacos服务以及“坑点”
45 1
|
1月前
|
运维 应用服务中间件 调度
|
3月前
|
缓存 Java Nacos
nacos服务注册问题之容器报错如何解决
Nacos是一个开源的、易于部署的动态服务发现、配置管理和服务管理平台,旨在帮助微服务架构下的应用进行快速配置更新和服务治理;在实际运用中,用户可能会遇到各种报错,本合集将常见的Nacos报错问题进行归纳和解答,以便使用者能够快速定位和解决这些问题。
|
4月前
|
应用服务中间件 nginx Docker
百度搜索:蓝易云【关于在容器中,nignx代理后端多个服务如何保证后端服务的地址不变呢?】
通过这种方式,您可以保证在容器中使用Nginx代理后端多个服务时,后端服务的地址不变,无论后端服务的容器如何重启或迁移。
33 0
|
4月前
|
Kubernetes 监控 测试技术
阿里云容器计算服务ACS功能测试
阿里云容器计算服务ACS功能测试,结合实际测试过程,总结一些使用经验。
阿里云容器计算服务ACS功能测试
|
5月前
|
Kubernetes Serverless 云栖大会
容器与Serverless的完美结合:全球首发的ACS服务让算力交付更加灵活自由
最近的一个重磅新闻刷爆技术圈,那就是阿里云发布了全球首个容器计算服务ACS(Alibaba Container Service),引起了技术圈的广泛关注。在加上近年来容器化技术在云计算领域得到了广泛应用,而且成为构建弹性、可扩展和可移植应用的关键工具。据官方消息,阿里云推出的ACS的最大亮点就是容器可以以Serverless形态交付算力,从而给使用者带来更加灵活、更加自由的体验感受,那么本文就带领大家来深入了解一下ACS这款新产品。
824 0
容器与Serverless的完美结合:全球首发的ACS服务让算力交付更加灵活自由
|
5月前
|
Kubernetes 安全 调度
深度解读:阿里云全球首发的容器计算服务 ACS 诞生背景、核心技术与应用场景
深度解读:阿里云全球首发的容器计算服务 ACS 诞生背景、核心技术与应用场景
68802 45
|
5月前
|
运维 Kubernetes Serverless
容器计算服务 ACS
简述容器计算服务 ACS
|
5月前
|
运维 Kubernetes 容器
容器计算服务 ACS
容器计算服务 ACS
84 1
|
5月前
|
弹性计算 Kubernetes 云计算
如何看待阿里云发布的全球首个容器计算服务 ACS?
如何看待阿里云发布的全球首个容器计算服务 ACS?
259 1