Introduction to the Symfony Service Container

Fabien Potencier

March 30, 2009

This article is part of a series on Dependency Injection in general and on a lightweight implementation of a Container in PHP in particular:

Until now in this series on Dependency Injection, we have talked about general concepts. The first two introductory articles were important to better understand the implementation we will talk about in this article and in the following ones. It is now time to dive into the Symfony 2 service container implementation.

The Dependency Injection Container in Symfony is managed by a class named sfServiceContainer. It is a very lightweight class that implements the basic features we talked about in the last article.

The Symfony Service Container is available as a standalone component in the symfony official Subversion repository: http://svn.symfony-project.com/components/dependency_injection/trunk/. Keep in mind that this component is still under heavy development, which means that things can change.

In Symfony speak, a service is any object managed by the container. In the Zend_Mail example from the last article, we had two of them: the mailer service and the mail_transport service:

class Container
{
  static protected $shared = array();
 
  protected $parameters = array();
 
  public function __construct(array $parameters = array())
  {
    $this->parameters = $parameters;
  }
 
  public function getMailTransport()
  {
    return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
      'auth'     => 'login',
      'username' => $this->parameters['mailer.username'],
      'password' => $this->parameters['mailer.password'],
      'ssl'      => 'ssl',
      'port'     => 465,
    ));
  }
 
  public function getMailer()
  {
    if (isset(self::$shared['mailer']))
    {
      return self::$shared['mailer'];
    }
 
    $class = $this->parameters['mailer.class'];
 
    $mailer = new $class();
    $mailer->setDefaultTransport($this->getMailTransport());
 
    return self::$shared['mailer'] = $mailer;
  }
}
 

If we make the Container class extend the sfServiceContainer Symfony class, we can simplify the code a bit:

class Container extends sfServiceContainer
{
  static protected $shared = array();
 
  protected function getMailTransportService()
  {
    return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
      'auth'     => 'login',
      'username' => $this['mailer.username'],
      'password' => $this['mailer.password'],
      'ssl'      => 'ssl',
      'port'     => 465,
    ));
  }
 
  protected function getMailerService()
  {
    if (isset(self::$shared['mailer']))
    {
      return self::$shared['mailer'];
    }
 
    $class = $this['mailer.class'];
 
    $mailer = new $class();
    $mailer->setDefaultTransport($this->getMailTransportService());
 
    return self::$shared['mailer'] = $mailer;
  }
}
 

That's not much, but it will give us a more powerful and clean interface. Here is the main changes we made:

  • The method names have been suffixed with Service. By convention, a service is to be defined by a method prefixed by get and suffixed by Service. Each service has a unique identifier, which is the underscored version of the method name without the prefix and suffix. By defining a getMailTransportService() method, we have defined a service named mail_transport.

  • The methods are now protected. We will see in a minute how to retrieve services from the container.

  • The container can be used as an array to get parameter values ($this['mailer.class']).

A service identifier must be unique and must be made of letters, numbers, underscores, and dots. Dots are useful to define "namespaces" within your container (for instance mail.mailer and mail.transport).

Let's see how to use the new container class:

require_once 'PATH/TO/sf/lib/sfServiceContainerAutoloader.php';
sfServiceContainerAutoloader::register();
 
$sc = new Container(array(
  'mailer.username' => 'foo',
  'mailer.password' => 'bar',
  'mailer.class'    => 'Zend_Mail',
));
 
$mailer = $sc->mailer;
 

Now, because the Container class extends the sfServiceContainer class, we can enjoy a cleaner interface:

  • Services are accessible via a uniform interface:

    if ($sc->hasService('mailer'))
    {
      $mailer = $sc->getService('mailer');
    }
     
    $sc->setService('mailer', $mailer);
     
  • As a shortcut, services are also accessible by using the class property notation:

    if (isset($sc->mailer))
    {
      $mailer = $sc->mailer;
    }
     
    $sc->mailer = $mailer;
     
  • Parameters are accessible via a uniform interface:

    if (!$sc->hasParameter('mailer_class'))
    {
      $sc->setParameter('mailer_class', 'Zend_Mail');
    }
     
    echo $sc->getParameter('mailer_class');
     
    // Override all parameters of the container
    $sc->setParameters($parameters);
     
    // Adds parameters
    $sc->addParameters($parameters);
     
  • As a shortcut, parameters are also accessible by using the container like an array:

    if (!isset($sc['mailer.class']))
    {
      $sc['mailer.class'] = 'Zend_Mail';
    }
     
    $mailerClass = $sc['mailer.class'];
     
  • You can also iterate over all services of a container:

    foreach ($sc as $id => $service)
    {
      echo sprintf("Service %s is an instance of %s.\n", $id, get_class($service));
    }
     

Using the sfServiceContainer class is very useful when you have a very small number of services to manage; even if you still need to do a lot of groundwork yourself, and duplicate a lot of code. But, if the number of services to be managed starts to grow beyond a few of them, we need a better way to describe the services.

That's why, most of the time, you don't use the sfServiceContainer class directly. It was nonetheless important to take some time to describe it as it is the corner stone of the Symfony dependency injection container implementation.

In the next article, we will have a look at the sfServiceContainerBuilder class, which eases the service definition process.

Discussion

gravatar Yuriy Voziy  — March 30, 2009 13:52   #1
Nice idea with using dots as namespaces.
gravatar Peter  — March 30, 2009 14:43   #2
Will any IDEs manage to autocomplete from the returned service? Fro the way the calls are made I think not
gravatar Pablo  — March 30, 2009 15:41   #3
@Peter

I guess you will have to use the phpDoc annotation:

/* var @mailer Zend_Mail */
gravatar Javi  — March 31, 2009 00:15   #4
So, in symfony 1.0 when you define factories.yml are you defining the main parameters of a "sfServiceContainer"?
gravatar Fabien  — March 31, 2009 06:55   #5
@Javi: You are right. The symfony 1.0 factories.yml configuration file is a contrived example of a hardcoded service container configuration file (although notice that is does not really used dependency injection). This will be replaced by a "real" service container with full dependency injection support in Symfony 2.0. The container will be described in details in the next articles.
gravatar Ian P. Christian (pookey)  — March 31, 2009 17:09   #6
I've given a brief introduction to more aspects of the new service container on my blog - http://pookey.co.uk/blog/archives/82-sfServiceDefinition-a-brief-introduction.html
gravatar Ken Marfilla  — April 04, 2009 07:41   #7
How do I know if the registered service has been instantiated already?