Achieving true Dependency Injection using Symfony Components

Symfony2, a high-performance PHP framework, leverages the Dependency Injection Container (DIC) pattern. Components within this framework interact with the DIC through a dependency injection interface. This design promotes decoupling by allowing components to remain agnostic about their dependencies. During initialization, the ‘Kernel’ class instantiates the DIC and subsequently injects it into various components. However, this approach inadvertently opens the door for the DIC to be utilized as a Service Locator.

Symfony2 even provides the ‘ContainerAware’ class for this very purpose. While many consider the Service Locator an anti-pattern within the Symfony2 context, I beg to differ. It presents itself as a more straightforward pattern compared to DI, proving particularly beneficial in simpler projects. Nevertheless, the simultaneous employment of both the Service Locator and DIC patterns within a single project undeniably constitutes an anti-pattern.

True Dependency Injection with Symfony Components

This article endeavors to guide you through building a Symfony2 application devoid of the Service Locator pattern. We shall adhere to a fundamental principle: only the DIC builder is privy to the intricacies of the DIC.

Demystifying the DIC

In the realm of Dependency Injection, the DIC assumes the role of a dependency orchestrator, defining and managing service dependencies. Services, in turn, expose an injection interface, enabling the DIC to fulfill their dependencies. The subject of Dependency Injection has been extensively covered in numerous articles, which I trust you’ve had the opportunity to explore. Therefore, let’s dispense with theoretical elaborations and delve straight into the crux of the matter. DI can be broadly classified into three primary types:

Symfony simplifies the configuration of injection structures through intuitive configuration files. Here’s a glimpse into how these three injection types can be configured:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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"

Laying the Foundation: Project Bootstrapping

Let’s embark on structuring our base application. In tandem, we’ll seamlessly integrate the Symfony DIC component.

1
2
3
4
5
6
7
8
9
$ 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 empower Composer’s autoloader to locate our custom classes residing within the ‘src’ folder, we can enhance the ‘composer.json’ file with an ‘autoloader’ property:

1
2
3
4
5
6
{
// ...
  "autoload": {
    "psr-4": { "": "src/" }
  }
}

Now, let’s bring our container builder to life while explicitly prohibiting container injections.

 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
37
38
39
40
41
// 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);
    }
}

In this instance, we leverage Symfony’s Config and Yaml components, the details of which can be found in the official documentation here. Furthermore, we establish a root path parameter, ‘app_root,’ for potential future use. The ‘get’ method overrides its counterpart in the parent class, modifying its default behavior to prevent the container from returning the “service_container.”

Our next order of business is to establish an entry point for our application.

1
2
3
4
// in www/index.php
require_once('../vendor/autoload.php');

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

This entry point serves as the gatekeeper for HTTP requests. We retain the flexibility to introduce additional entry points tailored for console commands, cron tasks, and more. Each entry point is entrusted with the responsibility of retrieving specific services and must possess knowledge of the DIC structure. This constitutes the sole location where we can request services from the container. From this juncture onward, our mission is to construct the application exclusively through DIC configuration files.

HttpKernel: The Web Component Backbone

HttpKernel (distinct from the framework kernel that presents the service locator predicament) will serve as the foundational component for the web-facing aspect of our application. A typical HttpKernel workflow unfolds as follows:

Green squares symbolize events.

HttpKernel relies on the HttpFoundation component for Request and Response objects, and the EventDispatcher component for its event-driven architecture. Initializing these components through DIC configuration files poses no particular challenges. HttpKernel, however, mandates initialization with EventDispatcher, ControllerResolver, and optionally RequestStack (catering to sub-requests) services.

Here’s the corresponding container configuration:

1
2
3
4
# in config/events.yml
services:
  dispatcher:
    class: Symfony\Component\EventDispatcher\EventDispatcher
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 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"]
1
2
3
4
#in config/services.yml
imports:
  - { resource: 'events.yml' }
  - { resource: 'kernel.yml' }

As you’ve likely observed, we employ the ‘factory’ property to instantiate the request service. The HttpKernel service receives a Request object as input and produces a Response object as output. This exchange can be neatly encapsulated within the front controller.

1
2
3
4
5
6
7
8
9
// 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();

Alternatively, the response can be defined as a service within the configuration, again utilizing the ‘factory’ property.

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

Subsequently, we can effortlessly retrieve it within the front controller.

1
2
3
4
5
6
7
// in www/index.php
require_once('../vendor/autoload.php');

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

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

The controller resolver service extracts the ‘_controller’ property from the attributes of the Request service to pinpoint the appropriate controller. While these attributes can be defined within the container configuration, the process is slightly more intricate due to the requirement of using a ParameterBag object in lieu of a simple array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 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"
# ...

Here’s the DefaultController class, complete with its defaultAction method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 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 these pieces in place, we should now have a functional application.

Admittedly, this controller is rather rudimentary as it lacks access to any services. The Symfony framework addresses this by injecting the DIC into controllers, enabling their use as service locators. However, we’re striving for a different approach. Therefore, let’s define the controller as a service and inject the request service into it. Here’s the configuration:

1
2
3
4
5
# in config/controllers.yml
services:
  controller.default:
    class: App\Controller\DefaultController
    arguments: [ "@request"]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 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"
# ...
1
2
3
4
5
6
#in config/services.yml

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

And the accompanying controller code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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, our controller enjoys access to the request service. You’ll notice that this scheme introduces circular dependencies. This functions seamlessly due to the DIC’s behavior of sharing services after creation but before method and property injections. Consequently, when the controller service is being created, the request service is already available.

Here’s a visual representation of the process:

However, this arrangement hinges on the request service being created first. When we fetch the response service within the front controller, the request service is the initial dependency initialized. Attempting to retrieve the controller service first would trigger a circular dependency error. This can be rectified using method or property injections.

Yet, another challenge awaits. The DIC will initialize every controller along with its dependencies. This implies that all existing services will be initialized, even if they are not required. Fortunately, the container offers lazy loading capabilities. The Symfony DI-component leverages ‘ocramius/proxy-manager’ to generate proxy classes. We need to establish a bridge between them.

1
$ composer require symfony/proxy-manager-bridge

And include it during the container building phase:

1
2
3
4
5
6
7
// in src/TrueContainer.php
//...
use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator;
// ...
    $container = new self();
    $container->setProxyInstantiator(new RuntimeInstantiator());
// ...

Now, we can define services with lazy loading behavior.

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

With this in place, controllers will only trigger the initialization of their dependent services when a method is actually invoked. Moreover, it circumvents circular dependency errors because the controller service will be shared before its actual initialization. Nonetheless, we must remain vigilant about avoiding circular references. In this scenario, we should refrain from injecting the controller service into the request service or vice versa. Since we inherently require the request service within controllers, let’s prevent injection into the request service during the container initialization phase. HttpKernel’s event system comes to our rescue.

Routing: Directing Traffic

Naturally, we desire distinct controllers to handle different requests. Enter the routing system. Let’s incorporate the Symfony routing component.

1
$ composer require symfony/routing

The routing component features the Router class, which can interpret routing configuration files. However, these configurations are essentially key-value pairs tailored for the Route class. The Symfony framework employs its own controller resolver from the FrameworkBundle, which injects the container into controllers using the ‘ContainerAware’ interface—precisely what we’re aiming to avoid. HttpKernel’s controller resolver returns a class object as is if it’s already present in the ‘_controller’ attribute as an array containing the controller object and the action method as a string (in reality, the controller resolver will return anything that is an array). Therefore, we must define each route as a service and inject the corresponding controller into it. Let’s introduce another controller service to illustrate this concept.

1
2
3
4
5
6
# in config/controllers.yml
# ...
  controller.page:
    lazy: true
    class: App\Controller\PageController
    arguments: [ "@request"]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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");
    }
}

The HttpKernel component provides the RouteListener class, which hooks into the ‘kernel.request’ event. Here’s a potential configuration utilizing lazy controllers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 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']
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 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"
1
2
3
4
5
6
# in config/events.yml
service:
  dispatcher:
      class: Symfony\Component\EventDispatcher\EventDispatcher
      calls:
        - [ addSubscriber, ["@router.listener"]]
1
2
3
4
5
6
#in config/services.yml
imports:
  - { resource: 'events.yml' }
  - { resource: 'kernel.yml' }
  - { resource: 'controllers.yml' }
  - { resource: 'routing.yml' }

Furthermore, our application necessitates a URL generator. Here’s the implementation:

1
2
3
4
5
6
7
# in config/routing.yml
# ...
  router.generator:
    class: Symfony\Component\Routing\Generator\UrlGenerator
    arguments:
      routes: "@route.collection"
      context: "@router.request_context"

The URL generator can be injected into controllers and rendering services. With that, we’ve established the foundation of our application. Any additional service can be defined following the same pattern, with the configuration file injected into the relevant controllers or event dispatcher. For instance, let’s examine configurations for Twig and Doctrine.

Twig: The Templating Powerhouse

Twig reigns supreme as the default template engine in the Symfony2 framework. Numerous Symfony2 components seamlessly integrate with Twig without requiring adapters, making it a natural choice for our application.

1
2
$ composer require twig/twig
$ mkdir src/App/View
1
2
3
4
5
6
7
8
# 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: The ORM of Choice

Doctrine stands as a prominent ORM within the Symfony2 ecosystem. While we retain the flexibility to opt for alternative ORMs, Symfony2 components are already equipped to leverage various Doctrine features.

1
2
$ composer require doctrine/orm
$ mkdir src/App/Entity
 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
# 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"
1
2
3
4
5
6
7
8
#in config/services.yml
imports:
  - { resource: 'events.yml' }
  - { resource: 'kernel.yml' }
  - { resource: 'controllers.yml' }
  - { resource: 'routing.yml' }
  - { resource: 'twig.yml' }
  - { resource: 'doctrine.yml' }

As an alternative to annotations, we can embrace YML and XML mapping configuration files. This simply entails utilizing the ‘createYAMLMetadataConfiguration’ and ‘createXMLMetadataConfiguration’ methods and specifying the path to the directory housing these configuration files.

Individually injecting every required service into each controller can quickly become tedious. To alleviate this, the DIC component offers abstract services and service inheritance. This enables us to define abstract controllers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 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
 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
37
38
39
40
41
42
43
44
45
46
47
48
// 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
{
    // ...
}

Symfony boasts an array of other valuable components, including Form, Command, and Assets. Developed as independent components, their integration using the DIC should pose no significant hurdles.

Tags: Enhancing Organization and Flexibility

The DIC incorporates a tagging system. Tags can be processed by Compiler Pass classes. While the Event Dispatcher component includes its own Compiler Pass to streamline event listener subscription, it relies on the ContainerAwareEventDispatcher class instead of the standard EventDispatcher class, rendering it unsuitable for our purposes. However, we can implement our own compiler passes for events, routing, security, and other functionalities.

As an illustration, let’s implement tags for the routing system. Currently, defining a route entails defining a route service in a route configuration file within the ‘config/routes’ directory and then adding it to the route collection service within the ‘config/routing.yml’ file. This approach feels disjointed, as we define router parameters and the router name in separate locations.

By introducing a tag system, we can simply specify a route name within a tag and add this route service to the route collection using the tag name.

The DIC component leverages compiler pass classes to modify container configurations before initialization. Let’s create our own compiler pass class for the router tag system.

 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
37
38
39
40
41
42
43
// 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));
        }
    }

} 
1
2
3
4
5
6
7
// in src/TrueContainer.php
//...
use CompilerPass\RouterTagCompilerPass;
// ...
    $container = new self();
    $container->addCompilerPass(new RouterTagCompilerPass());
// ...

Now, let’s refactor our configuration:

1
2
3
4
5
6
7
# in config/routing.yml
# …
  route.collection:
    class: Symfony\Component\Routing\RouteCollection
    tags:
      - { name: route_collection }
# ...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 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 now retrieve route collections using the tag name rather than the service name. This decoupling ensures that our route tag system operates independently of the specific configuration. Moreover, routes can be added to any collection service equipped with an ‘add’ method. Compiler passers can significantly streamline dependency configurations. However, it’s crucial to exercise caution, as they can introduce unexpected behavior into the DIC. It’s advisable to avoid modifying existing logic, such as altering arguments, method calls, or class names. Instead, focus on adding new functionality on top of the existing structure, as we’ve done with tags.

Conclusion: Embracing the DIC Pattern

We now have an application that adheres strictly to the DIC pattern, constructed entirely using DIC configuration files. As we’ve demonstrated, building a Symfony application in this manner doesn’t present any insurmountable obstacles. This approach also provides a clear and visual representation of your application’s dependencies. The primary reason developers often resort to using the DIC as a service locator stems from the relative ease of understanding the service locator concept. Consequently, vast codebases riddled with DICs masquerading as service locators are often a testament to this tendency.

The source code for this application can be found on GitHub.

Licensed under CC BY-NC-SA 4.0