Symfony2 and Dependency Injection

When I started using Symfony2 (autumn 2010) I realised, that some things about Dependency Injection design pattern are not really obvious. That's why I decided to write an article to make it at least a little bit more clear.

IMPORTANT NOTE

Since Symfony2 was released I strongly recommend you to read the official documentation for Service Container and take this article just as a quick reference.

Dependency Injection

If you don't know completelly anything about dependency injection I recommend your these resources:

The first article is more like an example why is Dependency Injection useful, while the second article is more focused on theory about Inversion of Control and Dependency Injection design patterns. The Wikipedia article is also worthy reading, especially the paragraph.

Purpose of this article is not to explain Dependency Injection but to show how it's implemented in Symfony2 and how to use it. The most common situation where you meet Dependency Injection is when you work with services.

Probably you'll deal with one of these situations:

Fortunately, it's always pretty simple.

How to access a service in a controller

This is probably the most common situation. Everytime you're accessing for example entity manager you're actually accessing a service in your service container.

 1 
 2 
 3 
$em = $this->container->get('doctrine.orm.entity_manager');
// or you can use just
$em = $this->get('doctrine.orm.entity_manager');

How to write my own service

At this point it's getting a little more complicated. When you're writing your own bundle Symfony2 allows you to set it up (load configuration, register services, ...) during the initialization process by creating so-called Extension class (for more information read Official Symfony2 Documentation - Semantic Configuration).

In other words, if we want to register our own service, we have to create an Extension class.

In my previous article on How to make a Twig extension for Symfony2 I made HelloExtension and in this tutorial I'm going to write almost the same 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 
// Bundle/HelloBundle/DependencyInjection/HelloExtension.php

namespace Bundle\HelloBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

class HelloExtension extends Extension
{

    public function load(array $configs, ContainerBuilder $container) {
        // here we'll define our service
    }

    /**
     * Was necessary in previous Symfony2 PR releases.
     * Symfony2 calls `load` method automatically now.
     *
     * public function getAlias() {
     *     return 'my_bundle';
     * }
     */
}

This class defines my_bundle namespace which we can use in configuration files.

# app/config/config.yml
my_bundle: ~

This triggers the `load` method in the `HelloExtension` class and passes all configuration defined for this namespace.

Now we'll modify load method to define our service.

Just to be clear, service can be any class, it doesn't has to extends any other classes.

Basically we have two options how to define services:

The first way, hardcoding definitions in PHP, is good only for very limited amount of services. I used it in one of my previous posts How to make a Twig extension for Symfony2 because there was just one very simple service.

The second way means writing service definitions in a separate XML (or YAML) file and loading it in our load method. I recommend writing an XML because almost all Symfony2 built-in bundles uses XML files and if you had hard time figuring out how to define something you can easily look how it's done by Symfony2 programmers.

So, lets modify load method to load extensions from an XML file.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
// Bundle/HelloBundle/DependencyInjection/HelloExtension.php
class HelloExtension extends Extension {

    public function load(array $config, ContainerBuilder $container) {
        // only load default services and parameters once
        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.xml');
    }

    // ...
}

write the XML definition:

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
# Bundle/HelloBundle/Resources/config/services.xml
<?xml version="1.0" ?>
<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="my_service" class="Bundle\HelloBundle\Services\MyService"></service>
    </services>

</container>

A quick explanation here:

Now, we'll write our service itself:
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
// Bundle/HelloBundle/Services/MyService.php
namespace Bundle\HelloBundle\Services;

class MyService {
    public function sum($n1, $n2) {
        return $n1 + $n2;
    }
}

Basically that's all. Now we can use MyService in any controller:

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
// Bundle/HelloBundle/Controller/HelloController.php
namespace Bundle\HelloBundle\Controller;

class HelloController extends Controller {

    public function indexAction() {
        $number = $this->get('my_service')->sum(12, 37);
        // this returns 49
    
        /*
        ...
        */
    }
}

Accessing our service is like accessing any other service.

 1 
$service = $this->get('my_service');

How to access other services inside my own service

Sometimes when you're writing your own service you realize, that you want to access database, request object, routes or whatever inside your service. The problem is, how?

In the XML service definition you can specify what arguments the Dependency Injection component will pass to your service constructor method. For example, lets say we want to write a service that accesses database and the routing loader object. To keep things simple we'll extend the MyService class used in the previous example and add a few lines to our XML service definition:

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
# Bundle/HelloBundle/Resources/config/services.xml
<?xml version="1.0" ?>
<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="my_service" class="Bundle\HelloBundle\Services\MyService">
            <argument type="service" id="doctrine.orm.entity_manager" />
            <argument type="service" id="routing.loader" />
        </service>
    </services>

</container>

and check what arguments (or what objects) are passed to the MyService::__constructor().

 1 
 2 
 3 
 4 
 5 
 6 
class MyService {
    public function __constructor() {
        print_r(func_get_args());exit;
    }
    // ...
}

as we could expect there are two arguments:

Array
(
    [0] => Doctrine\ORM\EntityManager Object
    [1] => Symfony\Bundle\FrameworkBundle\Routing\LazyLoader Object
)

That's great, so we can modify MyService class for the last time:

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Routing\LazyLoader;

class MyService {

    protected $em;
    protected $routeLoader;

    public function __construct(EntityManager $em, LazyLoader $routeLoader) {
        $this->em = $em;
        $this->routeLoader = $routeLoader;
    }

    public function doSomethingAmazing() {
        // $this->em - my entity manager
        // $this->routeLoader - my routing loader
    }

    // ...
}

In some situations you might want to pass to your service entire Service Container. It is possible but it's a very bad practise and you should always try to avoid it. This is very useful when your service needs to access a lot of other services.

 1 
 2 
 3 
 4 
# Bundle/HelloBundle/Resources/config/services.xml
<service id="my_service" class="Bundle\HelloBundle\Services\MyService">
    <argument type="service" id="service_container" />
</service>

A few words at the end

Most of these thing I figured out by examining Symfony2's source code because the official documentation is not complete yet. I don't want to claim that this is the best possible solution, If you found better leave me a comment.

Also this code snippets were written and tested on Symfony2 PR6 but should be the same for later Symfony2 releases Symfony2 PR7 PR9 PR12.

blog comments powered by Disqus