Symfony Service Container: Using XML or YAML to describe Services

Fabien Potencier

April 01, 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:

With the last article on Dependency Injection, you learned how to describe services with PHP code by using the sfServiceContainerBuilder class. Today, with the help of service loaders and dumpers, you will learn how to use XML or YAML to describe your services.

The Subversion repository has been updated with the code needed for today's tutorial. If you got the code from yesterday's repository, ou can just update it (svn up). If not, the repository is available at http://svn.symfony-project.com/components/dependency_injection/trunk/.

The Symfony Dependency Injection component provides helper classes that load services using "loader objects". By default, the component comes with two of them: sfServiceContainerLoaderFileXml to load XML files, and sfServiceContainerLoaderFileYaml to load YAML files.

But before diving into the XML and YAML notations, let's first have a look at another part of the Symfony Dependency Injection component: the "dumper objects". A service dumper takes a container object and convert it to another format. And of course, the component comes bundled with dumpers for the XML and YAML formats.

To introduce the XML format, let's convert yesterday's container service definitions to a container.xml file by using the sfServiceContainerDumperXml dumper class.

Remember the code we used to define the Zend_Mail service?

require_once '/PATH/TO/sfServiceContainerAutoloader.php';
sfServiceContainerAutoloader::register();
 
$sc = new sfServiceContainerBuilder();
 
$sc->
  register('mail.transport', 'Zend_Mail_Transport_Smtp')->
  addArgument('smtp.gmail.com')->
  addArgument(array(
    'auth'     => 'login',
    'username' => '%mailer.username%',
    'password' => '%mailer.password%',
    'ssl'      => 'ssl',
    'port'     => 465,
  ))->
  setShared(false)
;
 
$sc->
  register('mailer', '%mailer.class%')->
  addMethodCall('setDefaultTransport', array(new sfServiceReference('mail.transport')))
;
 

To convert this container to an XML representation, use the following code:

$dumper = new sfServiceContainerDumperXml($sc);
 
file_put_contents('/somewhere/container.xml', $dumper->dump());
 

A dumper class constructor takes a service container builder object as its first argument and the dump() method introspects the container services and converts them to another representation. If everything went fine, the container.xml file should look like the following XML snippet:

<?xml version="1.0" ?>
 
<container xmlns="http://symfony-project.org/2.0/container">
  <parameters>
    <parameter key="mailer.username">foo</parameter>
    <parameter key="mailer.password">bar</parameter>
    <parameter key="mailer.class">Zend_Mail</parameter>
  </parameters>
  <services>
    <service id="mail.transport" class="Zend_Mail_Transport_Smtp" shared="false">
      <argument>smtp.gmail.com</argument>
      <argument type="collection">
        <argument key="auth">login</argument>
        <argument key="username">%mailer.username%</argument>
        <argument key="password">%mailer.password%</argument>
        <argument key="ssl">ssl</argument>
        <argument key="port">465</argument>
      </argument>
    </service>
    <service id="mailer" class="%mailer.class%">
      <call method="setDefaultTransport">
        <argument type="service" id="mail.transport" />
      </call>
    </service>
  </services>
</container>
 

The XML format supports anonymous services. An anonymous service is a service that does not need a name and is defined directly in its use context. It can be very convenient when you need a service that won't be used outside of another one scope:

<service id="mailer" class="%mailer.class%">
  <call method="setDefaultTransport">
    <argument type="service">
      <service class="Zend_Mail_Transport_Smtp">
        <argument>smtp.gmail.com</argument>
        <argument type="collection">
          <argument key="auth">login</argument>
          <argument key="username">%mailer.username%</argument>
          <argument key="password">%mailer.password%</argument>
          <argument key="ssl">ssl</argument>
          <argument key="port">465</argument>
        </argument>
      </service>
    </argument>
  </call>
</service>
 

Loading back the container is dead simple thanks to the XML service loader class:

require_once '/PATH/TO/sfServiceContainerAutoloader.php';
sfServiceContainerAutoloader::register();
 
$sc = new sfServiceContainerBuilder();
 
$loader = new sfServiceContainerLoaderFileXml($sc);
$loader->load('/somewhere/container.xml');
 

As for dumpers, a loader takes a service container builder as its constructor first argument, and the load() method reads the file and registers the services into the container. The container is then useable as usual.

If you change the dumper code to use the sfServiceContainerDumperYaml class instead, you will have a YAML representation of your services:

require_once '/PATH/TO/sfYaml.php';
 
$dumper = new sfServiceContainerDumperYaml($sc);
 
file_put_contents('/somewhere/container.yml', $dumper->dump());
 

This will only work if you first load the sfYAML component (http://svn.symfony-project.com/components/yaml/trunk/) as it is required for the service container loader and dumper.

The previous container is represented like follows in YAML:

parameters:
  mailer.username: foo
  mailer.password: bar
  mailer.class:    Zend_Mail
 
services:
  mail.transport:
    class:     Zend_Mail_Transport_Smtp
    arguments: [smtp.gmail.com, { auth: login, username: %mailer.username%, password: %mailer.password%, ssl: ssl, port: 465 }]
    shared:    false
  mailer:
    class: %mailer.class%
    calls:
      - [setDefaultTransport, [@mail.transport]]
 

You can of course mix and match the loaders and the dumpers to convert any format to any other one:

// Convert an XML container service definitions file to a YAML one
$sc = new sfServiceContainerBuilder();
 
$loader = new sfServiceContainerLoaderFileXml($sc);
$loader->load('/somewhere/container.xml');
 
$dumper = new sfServiceContainerDumperYaml($sc);
file_put_contents('/somewhere/container.yml', $dumper->dump());
 

To keep this article short, I won't list all possibilities of the YAML or XML format. But you can easily learn them by converting an existing container and look at the output.

Using YAML or XML files for configuring your services allows you to create your services with a GUI (yet to be done...). But it also opens a lot more interesting possibilities.

One of the most important one is the ability to import other "resources". A resource can be any other configuration file:

<container xmlns="http://symfony-project.org/2.0/container">
  <imports>
    <import resource="default.xml" />
  </imports>
  <parameters>
    <!-- These parameters override the one defined in default.xml -->
  </parameters>
  <services>
    <!-- These service definitions override the one defined in default.xml -->
  </services>
</container>
 

The imports section lists resources that need to be included before the other sections are evaluated. By default, it looks for files with a path relative to the current file, but you can also pass an array of paths to look in as the second argument of the loader:

$loader = new sfServiceContainerLoaderFileXml($sc, array('/another/path'));
$loader->load('/somewhere/container.xml');
 

You can even embed a YAML definition file in an XML one by defining the class that is able to load the resource:

<container xmlns="http://symfony-project.org/2.0/container">
  <imports>
    <import resource="default.yml" class="sfServiceContainerLoaderFileYaml" />
  </imports>
</container>
 

And of course, the same goes for the YAML format:

imports:
  - { resource: default.xml, class: sfServiceContainerLoaderFileXml }
 

The import facility gives you a flexible way to organize your service definition files. It is also a great way to share and reuse definition files. Let's talk about the web session example we introduced in the first article. When you use web sessions in a test environment, the session storage object probably need to be mocked; on the contrary, and if you have several load-balanced web servers, the production environment need to store its sessions in a database like MySQL. One way to have a different configuration based on the environment is to create several different configuration files and import them as needed:

<!-- in /framework/config/default/session.xml -->
<container xmlns="http://symfony-project.org/2.0/container">
  <parameters>
    <parameter key="session.class">sfSessionStorage</parameter>
  </parameters>
 
  <!-- service definitions go here -->
</container>
 
<!-- in /project/config/session_test.xml -->
<container xmlns="http://symfony-project.org/2.0/container">
  <imports>
    <import resource="session.xml" />
  </imports>
 
  <parameters>
    <parameter key="session.class">sfSessionTestStorage</parameter>
  </parameters>
</container>
 
<!-- in /project/config/session_prod.xml -->
<container xmlns="http://symfony-project.org/2.0/container">
  <imports>
    <import resource="session.xml" />
  </imports>
 
  <parameters>
    <parameter key="session.class">sfMySQLSessionStorage</parameter>
  </parameters>
</container>
 

Using the right configuration is trivial:

$loader = new sfServiceContainerLoaderFileXml($sc, array(
  '/framework/config/default/',
  '/project/config/',
));
$loader->load('/somewhere/session_'.$environment.'.xml');
 

I can hear people crying about using XML to define the configuration, as XML is probably not the most readable configuration format on earth. Coming from a Symfony background, you could have written all the files in the YAML format. But you can also decouple the service definitions from their configuration. As you can import files form other ones, you can define services in a services.xml file, and store the related configuration in a parameters.xml one. You can also define parameters in a YAML file (parameters.yml). Eventually, there is a last built-in INI loader that is able to read parameters from a standard INI file:

<!-- in /project/config/session_test.xml -->
<container xmlns="http://symfony-project.org/2.0/container">
  <imports>
    <import resource="config.ini" class="sfServiceContainerLoaderFileIni" />
  </imports>
</container>
 
<!-- /project/config/config.ini -->
[parameters]
session.class = sfSessionTestStorage
 

It is not possible to define services in an INI file; only parameters can be defined and parsed.

These examples barely scratches the surface of the container loaders and dumpers features, but I hope this article has been a good overview of the power of the XML and YAML formats over the PHP one. And for those who are sceptic about the performance of a container that needs to load several files to be configured, I think you will be blown away by the next article. As this will be the last installment of this series on Dependency Injection, I will also talk about a nice way to visualize your service dependencies.

Discussion

gravatar Bernhard  — April 01, 2009 11:09   #1
Hi Fabien!

Thanks a lot for your informative articles about DI. This adds a lot to the presentation at the SymfonyCamp.

I want to point out an issue though: In the first and second XML snipped, there is this line in the mail.transport service definition:
<argument type="service" id="mail.transport" />
Is this a mistake, or didn't I grasp the concept?

Second, why don't you just rename "sfServiceContainerLoaderFileXml" to "sfServiceLoaderXml", "sfServiceContainerBuilder" to "sfServiceBuilder" etc.? These names would be much easier to remember. I know that the former is the most precise version, and java tends to have a lot of such names. On the other hand, java has packages and better IDE support, so memorizing all the complicated names is not that important.

My question: Is the gained flexibility and preciseness worth the long names? (same applies for some widget classes, btw)
gravatar Bernhard  — April 01, 2009 11:10   #2
Urgs, you should add support for line breaks to the comments :-)
gravatar Fabien  — April 01, 2009 11:43   #3
@Bernhard: Thanks for your feedback. The XML snippets had errors that are now corrected. I know that the names are quite long but also very descriptive. I don't know what other thinks about them?
gravatar Marijn Huizendveld  — April 01, 2009 11:44   #4
Wow Fabien, this really looks promising. I have the feeling this whole container approach will definitely help in making the framework even easier to tailor to specific project needs. Very much looking forward to Symfony 2.0!

Keep up de good work
gravatar Michal  — April 01, 2009 20:20   #5
Thank you, those articles are great, it would be nice if you could in future write more about technical details of symfony framework elements. Such technical articles helps to understand symfony better.

I have a question: you only described advantages of XML over YAML (not in other way). Does it mean you are more convinced of using XML instead of YAML? If so, what does it mean for Symfony 2? Will XML be preferred format for configuration?
gravatar Fabien  — April 01, 2009 21:09   #6
@Michal: I have just given some objectives facts to make a choice between XML and YAML. Of course, the YAML file is shorter are more readable. As I said in the article, you can mix and match XML and YAML files. It means that Symfony 2 won't be tied to one format or another. We will give users the choice.
gravatar JPhilip  — April 01, 2009 22:21   #7
My experience with Windsor an IoC for .NET is that the validation of the XML configuration is only partial. (ie it does not validate the class or method names passed in it)
By contrast, a DSL like Binsor in a statically typed language checks for that:
http://ayende.com/Blog/archive/2007/10/25/Binsor-2.0.aspx
gravatar Krzys  — April 14, 2009 13:19   #8
There is a typo in your listings. The port value dumped to XML or YAML is "true" and I strongly believe it should be "465".
(don't worry guys, Ive checked it and its a just a small typo not a bug in Fabien's-as-always-perfect-code :D).

Good work!
gravatar Jan  — April 14, 2009 14:29   #9
I would like to see how a ini file configuration would look like. Since you suggest separating namespaces with dots, something like:

parameters.mailer.username = foo

would be parsed into

$config['parameters']['mailer']['username']

and not

$config['parameters']['mailer.username']

as the container expects it.
gravatar Fabien  — April 14, 2009 17:20   #10
@Krzys: You are right. I have fixed it now. Thanks
gravatar Fabien  — April 14, 2009 17:21   #11
@Jan: an ini file can only be used for parameters. So your example must be written like this:

mailer.username = foo

and it will be parser as $config['mailer.username'] = 'foo'
gravatar Jan  — April 15, 2009 14:49   #12
Uhm...you're right...used Zend_Config too much i think (which transforms the dot-deparated options into nested arrays). Sometimes you forget how the native php functions work... :-/