Creating your own services for the Symfony2 DIC

I want you to know how to write a service and let the container handle it for you.

This post comes from a very cool evening with Jacopo Romei and David Funaro, two friends of mine, developers too.

A container?

Yes, a container: Symfony2 implements a dependency injection container able to load any service you need ( a service can be the DB connection, a Twitter WS, a mailer ) in the way you prefer.

Ever thought the factories.yml of symfony 1.4 was evil? The DIC basically does the same thing. But, the right way.

Why does a container is that great?

Because it lets you decide the behaviour of the framework.

Ever thought the sfActions were crap and wanted to build your own C of the MVC with symfony 1.4 in a simple way? A pain in the mess :)

With the DIC you are able to configure Symfony’s architecture.

Building your own service

I’ll show a really simple example.

To let the container manage your extension you simply need:

If you are familiar with the Sf2 sandbox you know it comes bundled with a simple application bundle, HelloBundle, which exposes the route:

1
/hello/:name

and renders a template saying hello to the name:

We will add the mood of our application to that page ;–)

First of all we need to tell Sf2 that we want to enable our service:

app/config/config_dev.yml
1
hello.service: ~

Then we have to build an extension able to load the configuration for our service.

Inside src/Application/HelloBundle we create a directory DependencyInjection and create the HelloExtension class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

namespace Application\HelloBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Resource\FileResource;

class HelloExtension extends Extension
{
    public function serviceLoad($config, ContainerBuilder $container)
    {
        $loader = new XmlFileLoader($container, __DIR__.'/../Resources/config');
        $loader->load('hello.xml');

        $container->setParameter('hello.service.mood', isset($config['mood']));
    }

    public function getAlias()
    {
        return 'hello';
    }

    public function getXsdValidationBasePath()
    {
        return __DIR__.'/../Resources/config/schema';
    }

    public function getNamespace()
    {
        return 'http://www.symfony-project.org/schema/dic/doctrine';
    }
}

If you take a closer look to the serviceLoad method

1
2
3
4
5
6
7
8
9
<?php

public function serviceLoad($config, ContainerBuilder $container)
{
    $loader = new XmlFileLoader($container, __DIR__.'/../Resources/config');
    $loader->load('hello.xml');

    $container->setParameter('hello.service.mood', isset($config['mood']));
}

you’ll notice that it accepts, as parameters, the configuration ( yes, the YML configuration, the one we left untouched with the ~ symbol ) and the container.

Why does our extension calls the serviceLoad method? That comes from the YAML configuration done before:

1
hello.service: ~

Remember? If we wrote hello.obama, we should have defined a obamaLoad method in our extension.

Then it creates an XMLFileLoader and loads the configuration of our services, which lies in the Resources/config folder, an XML file called hello.xml.

Then, it sets a parameter, for the service: it sets hello.service.mood; true if we have defined it in the configuration, false otherwise ( our situation ).

Ok, things will become clear watching at the hello.xml, located in src/Application/HelloBundle/Resources/config/hello.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" ?>

<container xmlns="http://www.symfony-project.org/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="hello.service.class">Application\HelloBundle\Service\Hello</parameter>
    </parameters>

    <services>
        <!--- Annotation Metadata Driver Service -->
        <service id="hello_service" class="%hello.service.class%">
          <argument>%hello.service.mood%</argument>
        </service>
    </services>
</container>

As you see, it defines a service ( hello_service ), which istantiates the class %hello.service.class%, a parameter defined some lines above, with the argument %hello.service.mood%.

Yes, that parameter comes from the HelloExtension!

Remember?

1
2
3
<?php

$container->setParameter('hello.service.mood', isset($config['mood']));

So, here we are passing to the XML the parameter %hello.service.mood% with false value.

Ok, we are close to the end, what’s missing there? Oh, our service!

In the XML we stated that the class of the hello_service service should be Application\HelloBundle\Service\Hello, so we only need to create it under src/Application/HelloBundle/Service/Hello.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace Application\HelloBundle\Service;

class Hello
{
  public function __construct($mood)
  {
    $this->mood = $mood;
  }

  public function __toString()
  {
    if ($this->mood)
    {
      return "sunshine reggae!";
    }

    return 'Oh no';
  }
}

as you see, the mood is passed by the DIC in the constructor*, and we have defined a __toString returning 2 different strings based on the value of the mood.

Ok, we’re done.

Open the dummy front controller bundled with the sandbox and pass a second argument to the template:

src/Application/HelloBundle/Controller/HelloController.php
1
2
3
4
5
6
7
8
<?php

public function indexAction($name)
{
    $hello = $this->get('hello_service');

    return $this->render('HelloBundle:Hello:index.twig', array('name' => $name, 'mood' => $hello));
}

then edit the twig template:

File /src/source/downloads/code/[src/Application/HelloBundle/Resource/views/Hello/index.twig] could not be found

So, let’s see it in action:

Now we can edit the service configuration:

1
2
hello.service:
  mood: true

and see what happens:

“Oh shit”… A bit lost? Here’s a recap!

Ok, let’s try to explain things again!

In the configuration of you application ( app/config/config_dev.yml ) you add to your application a new service ( hello.service ), with no parameters.

Then, when in the application you call the service hello_service, the DIC looks for an extension able to load the service, which is HelloExtension. It runs the method serviceLoad, which looks to an XML describing the service passing it the parameters you defined in the YAML configuration ( none ).

Then the DIC instantiate the service class with the parameters mapped in the XML ( $mood = false ).

The second time, we have defined $mood as true, so the container instantiates the class with a really simple:

1
2
3
<?php

$service = new Hello(true);

the result is that you get an instance of your service configured as you want.


In the mood for some more reading?

...or check the archives.