Back-end11 minute read

True Dependency Injection with Symfony Components

The Dependency Injection Container in Symfony2 allows components to be injected with their dependencies, and is often used as a Service Locator, which when combined with the DI-container pattern is considered to be an anti-pattern by many.

In this article, Toptal engineer Vasilii Lapin shows us how you can build a simple Symfony2 application using the DI-container, but without implementing the Service Locator pattern.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

The Dependency Injection Container in Symfony2 allows components to be injected with their dependencies, and is often used as a Service Locator, which when combined with the DI-container pattern is considered to be an anti-pattern by many.

In this article, Toptal engineer Vasilii Lapin shows us how you can build a simple Symfony2 application using the DI-container, but without implementing the Service Locator pattern.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Vasilii Lapin
Verified Expert in Engineering
12 Years of Experience

Vasilii has worked as a web architect and PHP back-end developer, specializing in NIX administration, and high-load, large scale projects.

Expertise

Share

Symfony2, a high performance PHP framework, uses Dependency Injection Container pattern where components provide a dependency injection interface for the DI-container. This allows each component to not care about other dependencies. The ‘Kernel’ class initializes the DI-container and injects it into different components. But this means DI-container can be used as a Service Locator.

Symfony2 even has the ‘ContainerAware’ class for that. Many hold the opinion that Service Locator is an anti-pattern in Symfony2. Personally, I do not agree. It is a simpler pattern compared to DI and it is good for simple projects. But the Service Locator pattern and the DI-container pattern combined in single project is definitely an anti-pattern.

True Dependency Injection with Symfony Components

In this article we will try to build a Symfony2 application without implementing Service Locator pattern. We will follow one simple rule: only DI-container builder can know about DI-container.

DI-container

In Dependency Injection pattern, DI-container define service dependencies and services can only give an interface for injection. There are many articles about Dependency Injection, and you probably have read all of them. So let’s not focus on the theory and just take a look at the basic idea. DI can be of 3 types:

In Symfony, injection structure can be defined using simple configuration files. Here’s how these 3 injection types can be configured:

services:
  my_service:
    class: MyClass
  constructor_injection_service:
    class: SomeClass1
    arguments: ["@my_service"]
  method_injection_service:
    class: SomeClass2
    calls:
      - [ setProperty, "@my_service" ]
  property_injection_service:
    class: SomeClass3
    properties:
      property: "@my_service"

Bootstrapping Project

Let’s create our base application structure. While we are at it, we will install Symfony DI-container component.

$ mkdir trueDI
$ cd trueDI
$ composer init
$ composer require symfony/dependency-injection
$ composer require symfony/config
$ composer require symfony/yaml
$ mkdir config
$ mkdir www
$ mkdir src

To make composer autoloader find our own classes in src folder, we can add ‘autoloader’ property in composer.json file:

{
// ...
  "autoload": {
    "psr-4": { "": "src/" }
  }
}

And let’s create our container builder and forbid container injections.

// in src/TrueContainer.php
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\ContainerInterface;

class TrueContainer extends ContainerBuilder {

    public static function buildContainer($rootPath)
    {
        $container = new self();
        $container->setParameter('app_root', $rootPath);
        $loader = new YamlFileLoader(
            $container,
            new FileLocator($rootPath . '/config')
        );
        $loader->load('services.yml');
        $container->compile();

        return $container;
    }

    public function get(
        $id, 
        $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE
    ) {
        if (strtolower($id) == 'service_container') {
            if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE 
                !== 
                $invalidBehavior
            ) {
                return;
            }
            throw new InvalidArgumentException(
                'The service definition "service_container" does not exist.'
            );
        }
        
        return parent::get($id, $invalidBehavior);
    }
}

Here we use the Config and the Yaml symfony components. You can find details in official documentation here. Also we defined the root path parameter ‘app_root’ just in case. The get method overloads the parent class’ default get behavior and prevents the container from returning the “service_container”.

Next, we need an entry point for the application.

// in www/index.php
require_once('../vendor/autoload.php');

$container = TrueContainer::buildContainer(dirname(__DIR__));

This one is meant to handle http requests. We can have more entry points for console commands, cron tasks and more. Each entry point is supposed to get certain services and should know about the DI-container structure. This is the only place where we can request services from the container. From this moment we will try to build this application only using DI-container configuration files.

HttpKernel

HttpKernel (not the framework kernel with the service locator problem) will be our base component for the web part of the application. Here is a typical HttpKernel workflow:

Green squares are events.

HttpKernel uses HttpFoundation component for Request and Response objects and EventDispatcher component for event system. There are no problems in initializing them with DI-container configuration files. HttpKernel has to be initialized with EventDispatcher, ControllerResolver, and optionally with RequestStack (for sub-requests) services.

Here is the container configuration for it:

# in config/events.yml
services:
  dispatcher:
    class: Symfony\Component\EventDispatcher\EventDispatcher
# in config/kernel.yml
services:
  request:
    class: Symfony\Component\HttpFoundation\Request
    factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ]
  request_stack:
    class: Symfony\Component\HttpFoundation\RequestStack
  resolver:
    class: Symfony\Component\HttpKernel\Controller\ControllerResolver
  http_kernel:
    class: Symfony\Component\HttpKernel\HttpKernel
    arguments: ["@dispatcher", "@resolver", "@request_stack"]
#in config/services.yml
imports:
  - { resource: 'events.yml' }
  - { resource: 'kernel.yml' }

As you can see, we use the ‘factory’ property to create the request service. HttpKernel service only gets Request object and returns Response object. It can be done in the front controller.

// in www/index.php
require_once('../vendor/autoload.php');

$container = TrueContainer::buildContainer(dirname(__DIR__));

$HTTPKernel = $container->get('http_kernel');
$request = $container->get('request');
$response = $HTTPKernel->handle($request);
$response->send();

Or the response can be defined as a service in the config by using the ‘factory’ property.

# in config/kernel.yml
# ...
  response:
    class: Symfony\Component\HttpFoundation\Response
    factory: [ "@http_kernel", handle]
    arguments: ["@request"]

And then we just get it in the front controller.

// in www/index.php
require_once('../vendor/autoload.php');

$container = TrueContainer::buildContainer(dirname(__DIR__));

$response = $container->get('response');
$response->send();

The controller resolver service gets ‘_controller’ property from attributes of the Request service to resolve controller. These attributes can be defined in container config, but it looks a bit more tricky because we have to use a ParameterBag object instead of a simple array.

# in config/kernel.yml
# ...
  request_attributes:
    class: \Symfony\Component\HttpFoundation\ParameterBag
    calls:
      - [ set, [ _controller, \App\Controller\DefaultController::defaultAction ]]
  request:
    class: Symfony\Component\HttpFoundation\Request
    factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ]
    properties:
      attributes: "@request_attributes"
# ...

And here is the DefaultController class with defaultAction method.

// in src/App/Controller/DefaultController.php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;

class DefaultController {

    function defaultAction()
    {
        return new Response("Hello cruel world");
    }
}

With all of these in place, we should have a working application.

This controller is pretty useless because it doesn’t have access to any service. In Symfony framework, this problem is solved by injecting a DI-container in a controller and using it as a service locator. We won’t do that. So let’s define the controller as a service and inject the request service in it. Here is the configuration:

# in config/controllers.yml
services:
  controller.default:
    class: App\Controller\DefaultController
    arguments: [ "@request"]
# in config/kernel.yml
# ...
  request_attributes:
    class: \Symfony\Component\HttpFoundation\ParameterBag
    calls:
      - [ set, [ _controller, ["@controller.default", defaultAction ]]]
  request:
    class: Symfony\Component\HttpFoundation\Request
    factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ]
    properties:
      attributes: "@request_attributes"
# ...
#in config/services.yml

imports:
  - { resource: 'events.yml' }
  - { resource: 'kernel.yml' }
  - { resource: 'controllers.yml' }

And the controller code:

// in src/App/Controller/DefaultController.php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class DefaultController {

    /** @var Request */
    protected $request;

    function __construct(Request $request)
    {
        $this->request = $request;
    }

    function defaultAction()
    {
        $name = $this->request->get('name');
        return new Response("Hello $name");
    }
}

Now the controller has access to the request service. As you can see, this scheme has circular dependencies. It works because the DI-container shares service after creation and before method and property injections. So when the controller service is creating, the request service already exists.

Here’s how it works:

But this works only because the request service is created first. When we get response service in the front controller, the request service is the first initialized dependency. If we try to get the controller service first, it will cause a circular dependency error. It can be fixed by using method or property injections.

But there is another problem. DI-container will initialize each controller with dependencies. So it will initialize all existed services even if they are not needed. Fortunately, the container has lazy loading functionality. Symfony DI-component uses ‘ocramius/proxy-manager’ for proxy classes. We have to install a bridge between them.

$ composer require symfony/proxy-manager-bridge

And define it at the container building stage:

// in src/TrueContainer.php
//...
use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator;
// ...
    $container = new self();
    $container->setProxyInstantiator(new RuntimeInstantiator());
// ...

Now we can define lazy services.

# in config/controllers.yml
services:
  controller.default:
    lazy: true
    class: App\Controller\DefaultController
    arguments: [ "@request" ]

So controllers will cause initialization of depended services only when an actual method is called. Also, it avoids circular dependency error because a controller service will be shared before actual initialization; although we still have to avoid circular references. In this case we should not inject the controller service in the request service or the request service into the controller service. Obviously we need a request service in controllers, so let’s avoid an injection in the request service on the container initiation stage. HttpKernel has events system for this purpose.

Routing

Apparently we want to have different controllers for different requests. So we need a routing system. Let’s install the symfony routing component.

$ composer require symfony/routing

The routing component has class Router which can use routing configuration files. But these configurations are just key-value parameters for the Route class. Symfony framework uses its own controller resolver from the FrameworkBundle which injects container in controllers with the ‘ContainerAware’ interface. This is exactly what we are trying to avoid. HttpKernel controller resolver returns class object as is if it already exists in the ‘_controller’ attribute as array with controller object and action method string (actually, the controller resolver will return it as is if it is just an array). So we have to define each route as a service and inject a controller in it. Let’s add some other controller service to see how it works.

# in config/controllers.yml
# ...
  controller.page:
    lazy: true
    class: App\Controller\PageController
    arguments: [ "@request"]
// in src/App/Controller/PageController.php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class PageController {

    /** @var Request */
    protected $request;

    function __construct(Request $request)
    {
        $this->request = $request;
    }

    function defaultAction($id)
    {
        return new Response("Page $id doesn’t exist");
    }
}

HttpKernel component has the RouteListener class which uses ‘kernel.request’ event. Here is one possible configuration with lazy controllers:

# in config/routes/default.yml
services:
  route.home:
    class: Symfony\Component\Routing\Route
    arguments:
      path: /
      defaults:
        _controller: ["@controller.default", 'defaultAction']
  route.page:
    class: Symfony\Component\Routing\Route
    arguments:
      path: /page/{id}
      defaults:
        _controller: ["@controller.page", 'defaultAction']
# in config/routing.yml
imports:
  - { resource: ’routes/default.yml' }

services:
  route.collection:
    class: Symfony\Component\Routing\RouteCollection
    calls:
      - [ add, ["route_home", "@route.home"] ]
      - [ add, ["route_page", "@route.page"] ]
  router.request_context:
      class: Symfony\Component\Routing\RequestContext
      calls:
        - [ fromRequest, ["@request"] ]
  router.matcher:
    class: Symfony\Component\Routing\Matcher\UrlMatcher
    arguments: [ "@route.collection", "@router.request_context" ]
  router.listener:
    class: Symfony\Component\HttpKernel\EventListener\RouterListener
    arguments:
      matcher: "@router.matcher"
      request_stack: "@request_stack"
      context: "@router.request_context"
# in config/events.yml
service:
  dispatcher:
      class: Symfony\Component\EventDispatcher\EventDispatcher
      calls:
        - [ addSubscriber, ["@router.listener"]]
#in config/services.yml
imports:
  - { resource: 'events.yml' }
  - { resource: 'kernel.yml' }
  - { resource: 'controllers.yml' }
  - { resource: 'routing.yml' }

Also we need a URL generator in our application. Here it is:

# in config/routing.yml
# ...
  router.generator:
    class: Symfony\Component\Routing\Generator\UrlGenerator
    arguments:
      routes: "@route.collection"
      context: "@router.request_context"

URL generator can be injected into controller and rendering services. Now we have a base application. Any other service can be defined in the same way the configuration file is injected into certain controllers or event dispatcher. For example, here are some configurations for Twig and Doctrine.

Twig

Twig is the default template engine in Symfony2 framework. Many Symfony2 components can use it without any adapters. So it’s an obvious choice for our application.

$ composer require twig/twig
$ mkdir src/App/View
# in config/twig.yml
services:
  templating.twig_loader:
    class: Twig_Loader_Filesystem
    arguments: [ "%app_root%/src/App/View" ]
  templating.twig:
    class: Twig_Environment
    arguments: [ "@templating.twig_loader" ]

Doctrine

Doctrine is an ORM used in Symfony2 framework. We can use any other ORM, but Symfony2 components already can use many Docrine features.

$ composer require doctrine/orm
$ mkdir src/App/Entity
# in config/doctrine.yml
parameters:
  doctrine.driver: "pdo_pgsql"
  doctrine.user: "postgres"
  doctrine.password: "postgres"
  doctrine.dbname: "true_di"
  doctrine.paths: ["%app_root%/src/App/Entity"]
  doctrine.is_dev: true

services:
  doctrine.config:
    class: Doctrine\ORM\Configuration
    factory: [ Doctrine\ORM\Tools\Setup, createAnnotationMetadataConfiguration ]
    arguments:
      paths: "%doctrine.paths%"
      isDevMode: "%doctrine.is_dev%"
  doctrine.entity_manager:
    class: Doctrine\ORM\EntityManager
    factory: [ Doctrine\ORM\EntityManager, create ]
    arguments:
      conn:
        driver: "%doctrine.driver%"
        user: "%doctrine.user%"
        password: "%doctrine.password%"
        dbname: "%doctrine.dbname%"
      config: "@doctrine.config"
#in config/services.yml
imports:
  - { resource: 'events.yml' }
  - { resource: 'kernel.yml' }
  - { resource: 'controllers.yml' }
  - { resource: 'routing.yml' }
  - { resource: 'twig.yml' }
  - { resource: 'doctrine.yml' }

We can also use YML and XML mapping configuration files instead of annotations. We just need to use ‘createYAMLMetadataConfiguration’ and ‘createXMLMetadataConfiguration’ methods and set path to a folder with these config files.

It can quickly become very annoying to inject every needed service in every controller individually. To make it a little better DI-container component has abstract services and service inheritance. So we can define some abstract controllers:

# in config/controllers.yml
services:
  controller.base_web:
    lazy: true
    abstract: true
    class: App\Controller\Base\WebController
    arguments:
      request:  "@request"
      templating:  "@templating.twig"
      entityManager:  "@doctrine.entity_manager"
      urlGenerator:  "@router.generator"

  controller.default:
    class: App\Controller\DefaultController
    parent: controller.base_web
    
  controller.page:
    class: App\Controller\PageController
    parent: controller.base_web
// in src/App/Controller/Base/WebController.php
namespace App\Controller\Base;

use Symfony\Component\HttpFoundation\Request;
use Twig_Environment;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Routing\Generator\UrlGenerator;

abstract class WebController
{
    /** @var Request */
    protected $request;
    
    /** @var Twig_Environment */
    protected $templating;
    
    /** @var EntityManager */
    protected $entityManager;
    
    /** @var UrlGenerator */
    protected $urlGenerator;

    function __construct(
        Request $request, 
        Twig_Environment $templating, 
        EntityManager $entityManager, 
        UrlGenerator $urlGenerator
    ) {
        $this->request = $request;
        $this->templating = $templating;
        $this->entityManager = $entityManager;
        $this->urlGenerator = $urlGenerator;
    }
}

// in src/App/Controller/DefaultController
// …
class DefaultController extend WebController
{
    // ...
}

// in src/App/Controller/PageController
// …
class PageController extend WebController
{
    // ...
}

There are many other useful Symfony components like Form, Command, and Assets. They were developed as independent components so their integration using DI-container should not be a problem.

Tags

DI-container also has a tags system. Tags can be processed by Compiler Pass classes. The Event Dispatcher component has its own Compiler Pass to simplify event listener subscription, but it uses ContainerAwareEventDispatcher class instead of EventDispatcher class. So we can’t use it. But we can implement our own compiler passes for events, routes, security and any other purpose.

For example, let’s implement tags for the routing system. Now to define a route we have to define a route service in a route config file in the config/routes folder and then add it to the route collection service in the config/routing.yml file. It looks inconsistent because we define router parameters in one place and a router name in another.

With tag system, we can just define a route name in a tag and add this route service to the route collection using a tag name.

The DI-container component uses compiler pass classes to make any modification to a container configuration before actual initialisation. So let’s implement our compiler pass class for the router tag system.

// in src/CompilerPass/RouterTagCompilerPass.php
namespace CompilerPass;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

class RouterTagCompilerPass implements CompilerPassInterface
{
    /**
     * You can modify the container here before it is dumped to PHP code.
     *
     * @param ContainerBuilder $container
     */
    public function process(ContainerBuilder $container)
    {
        $routeTags = $container->findTaggedServiceIds('route');

        $collectionTags = $container->findTaggedServiceIds('route_collection');

        /** @var Definition[] $routeCollections */
        $routeCollections = array();
        foreach ($collectionTags as $serviceName => $tagData)
            $routeCollections[] = $container->getDefinition($serviceName);

        foreach ($routeTags as $routeServiceName => $tagData) {
            $routeNames = array();
            foreach ($tagData as $tag)
                if (isset($tag['route_name']))
                    $routeNames[] = $tag['route_name'];
            
            if (!$routeNames)
                continue;

            $routeReference = new Reference($routeServiceName);
            foreach ($routeCollections as $collection)
                foreach ($routeNames as $name)
                    $collection->addMethodCall('add', array($name, $routeReference));
        }
    }

} 
// in src/TrueContainer.php
//...
use CompilerPass\RouterTagCompilerPass;
// ...
    $container = new self();
    $container->addCompilerPass(new RouterTagCompilerPass());
// ...

Now we can modify our configuration:

# in config/routing.yml
# …
  route.collection:
    class: Symfony\Component\Routing\RouteCollection
    tags:
      - { name: route_collection }
# ...
# in config/routes/default.yml
services:
  route.home:
    class: Symfony\Component\Routing\Route
    arguments:
      path: /
      defaults:
        _controller: ["@controller.default", 'defaultAction']
    tags:
      - { name: route, route_name: 'route_home' }

  route.page:
    class: Symfony\Component\Routing\Route
    arguments:
      path: /page/{id}
      defaults:
        _controller: ["@controller.page", 'defaultAction']
    tags:
      - { name: route, route_name: 'route_page' }

As you can see, we get route collections by the tag name instead of service name, so our route tag system doesn’t depend on the actual configuration. Also, routes can be added to any collection service with an ‘add’ method. Compiler passers can significantly simplify dependencies’ configurations. But they can add an unexpected behavior to the DI-container, so it’s better to not modify existed logic like changing arguments, method calls, or class names. Just add a new one over existed as we did by using tags.

Wrap Up

We now have an application that uses only DI container pattern, and it is built using only DI-container configuration files. As you can see, there are no serious challenges in building a Symfony application this way. And you can simply visualize all your application dependencies. The only reason why people use DI-container as a service locator is that a service locator concept is easier to understand. And a huge code base with DI-container used as a service locator is probably a consequence of that reason.

You can find the source code of this application on GitHub.

Hire a Toptal expert on this topic.
Hire Now
Vasilii Lapin

Vasilii Lapin

Verified Expert in Engineering
12 Years of Experience

Eindhoven, Netherlands

Member since December 3, 2015

About the author

Vasilii has worked as a web architect and PHP back-end developer, specializing in NIX administration, and high-load, large scale projects.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.