service container


Modern PHP programs are all objects. One object can be responsible for sending the email, and another object allows you to persist the information to the database. In the program, you can create an object to manage product inventory, or use another object to process data from a third-party API. The conclusion is that modern programs can do many things, and programs are composed of many objects organized together to handle their own tasks.

This chapter talks about a special PHP object in Symfony, which helps you instantiate, organize and retrieve many objects in the program. This object, called a "service container," allows you to standardize and centralize the organization of objects in your program. Containers make things simple, they are very fast, and they emphasize architecturally improving code reusability while reducing coupling. Since all Symfony classes use containers, you will learn how to extend, configure, and use objects in Symfony. From a large perspective, service containers are the biggest contributor to Symfony's speed and scalability.

Finally, configuring and using the service container is simple. After studying this chapter, you can easily create your own objects through the service container, and you can also customize any object in the third-party bundle. You'll start writing reusable, testable, loosely coupled code because service containers make it easy to write good code.

If you want to know more after reading this chapter, please refer to Dependency Injection Component.

What is a service

Simply put, a service (Service) can be any object that performs "global" tasks. It is a proper noun in computer science, used to describe an object created "to complete a certain task (such as sending an email)." In the program, when you need a specific function provided by a service, you can access the service at any time. You don't need to do anything special when creating a service: just write a PHP class that performs a task. Congratulations, you have created a service!

As a principle, if a PHP object wants to become a service, it must be available in the global scope of the program. An independent Mailer service is used "globally" to send email messages. However, the Message information objects it transmits are not not services. Similarly, a Product object is not a service, but an object that can persist products into the database is a service.

This shows what? The advantage of thinking about the problem from a "service" perspective is that you already want to separate each function in the program into a series of services. Since each service only does one thing, you can easily access the service whenever you need it. Testing and configuring each service becomes easier because they are separated from other functionality in your application. This concept is called Service-Oriented Architecture and is not exclusive to Symfony or PHP. "Structuring" your program through a set of independent service classes is a time-tested and well-known best practice in object-oriented programming. This skill is the key to becoming a good developer in any language.

What is a service container?

The service container (Service Container/dependency injection container) is a PHP object that manages the instantiation of services (i.e. objects).

For example, suppose you have a simple class for sending email messages. Without a service container, you would have to manually create objects when needed.

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

It's easy. A fictitious Mailer mail service class that allows you to configure the method of sending mail (such as sendmail, or smtp, etc.). But what if you want to use your mail service elsewhere? You certainly don't want to reconfigure the Mailer object every time. What if you want to change the way mail is transmitted and change all sendmail in the entire program to smtp? You have to find all the places where Mailer is created and update it manually.

Creating and configuring services in containers

The best answer to the above question is to let the service container create the Mailer object for you. In order for the container to work properly, you first need to teach it how to create the Mailer service. This is achieved through configuration, using YAML, XML or PHP:

PHP:// app/config/services.phpuse Symfony\Component\DependencyInjection\Definition; $container->setDefinition('app.mailer', new Definition(
    'AppBundle\Mailer',
    array('sendmail')));
XML:<!-- 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>
YAML:# app/config/services.ymlservices:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    [sendmail]

When Symfony initializes, it establishes services based on the configuration information Container (the default configuration file is app/config/config.yml). The correct configuration file to be loaded is indicated by the AppKernel::registerContainerConfiguration() method, which loads a "specific environment" configuration file (such as config_dev.yml is for the dev development environment, and config_prod.yml is for the production environment).

An instance of the Acme\HelloBundle\Mailer object is already available through the service container. Containers can be obtained directly from any standard Symfony controller through the get() shortcut method.

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

When you request the app.mailer service from the container, the container constructs the object and returns it (after instantiation). This is another benefit of using service containers. That is, a service is not constructed unless needed. If you define a service but it is never used during a request, the service will not be created. This saves memory and makes programs run faster. This also means that there is rarely a performance hit when defining a large number of services. Services that are never used will never be constructed.

By the way, the Mailer service is only created once, and the same instance is returned every time you request it. This should meet most of your needs (the behavior is flexible and powerful), but later you will need to learn how to configure a service with multiple instances, see How to define a non-shared service article.

In this example, the controller inherits Symfony's Controller base class, giving you an opportunity to use the service container. You can find and remove it from the container through the get() methodapp.mailer Service.

Service Parameters

The process of creating a new service through a container is very simple and straightforward. Parameters can make the definition of services more flexible and orderly:

PHP:// app/config/services.phpuse Symfony\Component\DependencyInjection\Definition; $container->setParameter('app.mailer.transport', 'sendmail'); $container->setDefinition('app.mailer', new Definition(
    'AppBundle\Mailer',
    array('%app.mailer.transport%')));
XML:<!-- 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>
YAML:# app/config/services.ymlparameters:
    app.mailer.transport:  sendmailservices:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    ['%app.mailer.transport%']


The result is the same as before. The difference lies in how you define the service. By surrounding app.mailer.transport with % percent signs, the container knows to look for the parameter corresponding to that name. When the container itself is generated, the value of each parameter will be restored to the service definition.

If you want to use a string starting with @ as a parameter value in a YAML file (such as a very secure email password), you You need to add another @ symbol for escaping (this situation is only applicable in YAML format configuration files)

# app/config/parameters.ymlparameters:    # This will be parsed as string '@securepass'
    mailer_password: '@@securepass'

Configuration parameters The percent sign in (parameter) or method parameter (argument) must also be escaped with another %:

The purpose of the parameters is to pass information to the service. Of course, if there are no parameters, there will be no problem. But using parameters has the following benefits:

  • Separate and organize all service "options" under a unified "parameter" key

  • Parameter values ​​can be used in multiple service definitions

  • When creating services in a bundle, using parameters can make it easy to customize the service in the global program

The choice is yours whether to use parameters. High-quality third-party bundles always use parameters because they want to make the services stored in the container more "configurable". Of course, you may not need the flexibility that parameters bring.

Array Parameters

Parameters can contain arrays, see Array Parameters (Array Parameters).

Reference (injection) service

At this point, the original app.mailer service is simple: it has only one parameter in the constructor , so it is easy to configure. You can expect that the real power of containers starts to show up when you create a service that depends on other services in one or more containers.

For example, you have a new service, NewsletterManager, that helps you manage and send email messages to a set of addresses. app.mailer The service can already send emails, so you can use it in NewsletterManager to take care of the message delivery part. This class looks like the following:

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

If you do not use a service container, you can easily create a NewsletterManager in the controller:

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

It is possible to implement it this way, But what if you want to add a second or third constructor parameter to the NewsletterManager class in the future? What if you decide to refactor your code and rename this class? In both cases, you need to find each place where the NewsletterManager class is instantiated, and then manually customize it. There is no doubt that the service container provides a more attractive way of processing:

PHP:// app/config/services.phpuse 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'))));
XML:<!-- 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>
YAML:# app/config/services.ymlservices:
    app.mailer:        # ...
    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        arguments: ['@app.mailer']

In YAML, the special @app.mailer syntax tells the container to find a service named app.mailer and then pass this object to NewsletterManager 's constructor parameters. In this example, the specified app.mailer service does exist. If it does not exist, an exception is thrown. However you can mark dependencies as optional - this topic is discussed in the next section.

References (to services) are a powerful tool that allow you to create independent services but with well-defined dependencies. In this example, the app.newsletter_manager service needs to rely on the app.mailer service in order to implement its functionality. When you define this dependency in the service container, the container hosts all the work of instantiating this class.

Optional dependency: Setter injection

Injecting dependent objects into the constructor is one way to ensure that the dependency can be utilized (otherwise the constructor cannot execute ). But for a class, if it has an optional dependency, then "setter injection" is a better solution. This means using a class method to inject dependencies, rather than a constructor. The class might look like this:

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

The service definition needs to be adjusted accordingly for setter injection:

PHP:// app/config/services.phpuse 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'),));
XML:<!-- 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>

YAML:# app/config/services.ymlservices:
    app.mailer:        # ...
    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        calls:            - [setMailer, ['@app.mailer']]

The process implemented in this section is called "constructor injection" and "setter injection". In addition, Symfony's container system also supports property injection.

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