Symfony2: Controller as Service

Background

I am recreating Lime Thinking's current website using Symfony2, much of the site is made up of what are effectively static pages. The main content of the page is stored as an XML file which then has the overall XSL template applied to it to add the common parts of the site.

For the Symfony2 version I am using Twig rather than XSL and XML as this the default template engine. So my first controller is probably an atypical first controller to use. I want it to covert the current XML file to a twig template which references the overall Twig template and then render this as the response. The details of this process are very specific to our particular process and I will not bother you with, however I did find some interesting details of using Symfony2 controllers in the process which will share here.

First Controller

I started by using a controller which extended the base controller class, I needed a response object as a Symfony2 controller must return one, it also needed a request object as I was getting details of which XML file to use from a GET parameter. According to the book by extending the base controller I should just be able to retrieve both of these from the Dependency Injection container, so that my controller could look something like this:
<?php

namespace LimeThinking\SpringBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class StaticController extends Controller
{


    protected $response;
    protected $request;

    public function __construct()
    {
        $this->response = $this->container->get('response');
        //can be shortened to $this->get('response');
        $this->request  = $this->container->get('request');
        //can be shortened to $this->get('request');
    }


    public function indexAction()
    {
        //do stuff to create response
        return $this->response;
    }


}

Unfortunately in the version I was using (PR7) neither of these objects was actually available as a service, I could just ignore Dependency Injection and get my controller working in the following way:

<?php

namespace LimeThinking\SpringBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class StaticController extends Controller
{


    protected $response;
    protected $request;

    public function __construct()
    {
        $this->response = new Response;
        $this->request  = new Request
    }


    public function indexAction()
    {
        //do stuff to create response
        return $this->response;
    }


}

Not really in the spirit of using Dependency Injection though. I found that I could add these services to my config.yml so that they would be available, adding the following allowed the first version of the controller to work:

services:
    response:
        class:        Symfony\Component\HttpFoundation\Response
    request:
        class:        Symfony\Component\HttpFoundation\Request  

Controller as Service

Whilst this is an improvement, it is still effectively Dependency Injection by container injection which is not what I want as it has many disadvantages as a form of Dependency Injection (I intend a follow up article with more details on why so will not discuss here). I would much rather be using constructor injection to get these objects into my controller. Fortunately, this is possible if I move away from extending the base class and instead define my controller as a service, I can then define my services within a bundle and inject them into the controller how I see fit.

In order to do this there are several steps, the initial further complication may not make the end result seem initially worth it as it is a simple example, but the additional control you get is in the long run. The following steps need to be taken:

  1. Change the way the controller is called in the routing config.
  2. Add a class to tell the framework how to load your service configuration.
  3. Modify the controller.
  4. Write a services configuration file.

Calling the service

Services are called slightly differently in the routing.yml file so the following:

static:
    pattern:  /{part}/
    defaults: { _controller: LimeThinkingSpringBundle:Static:index }

needed changing to this

static:
    pattern:  /{part}/
    defaults: { _controller: lime_thinking_spring.static:indexAction}

Service Configuration Loading

Doing this means that instead of the controller just being created directly it is requested from the Dependency Injection container, in order for this to be possible it needs to have had a service configuration file registered with it. We can do this by adding the following class, this is a very simplified version which will not allow for overriding of its settings:

<?php
namespace LimeThinking\SpringBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\Config\FileLocator;

class LimeThinkingSpringExtension extends Extension
{

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

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


}
?>

This is called automatically as long as it is in a folder called DependencyInjection in the root of the bundle and is named BundleNameExtension. More complicated loading strategies can be found by digging about in the equivalent file in some of the other included bundles. This is enough to get going for now though.

The Controller

If we now modify our controller to be like this:
<?php

namespace LimeThinking\SpringBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class StaticController
{


    protected $response;
    protected $request;

    public function __construct(Response $response, Request $request)
    {
        $this->response = $response;
        $this->request   = $request;
    }


    public function indexAction()
    {
        //do stuff to create response
        return $this->response;
    }


}

Note that it no longer extends the base controller and that we are injecting our dependencies via the constructor

Service Configuration File

Now by adding the following servcies.xml configuration file we can define the service routed to and its dependencies.

<?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="lime_thinking_spring.response" 
                 class="Symfony\Component\HttpFoundation\Response"/>      
        <service id="lime_thinking_spring.response" 
                 class="Symfony\Component\HttpFoundation\Request"/> 

        <service id="lime_thinking_spring.static" 
                 class="LimeThinking\SpringBundle\Controller\StaticController">
            <argument type="service" id="lime_thinking_spring.response" />
            <argument type="service" id="lime_thinking_spring.request" />
        </service>        
        
    </services>

</container>

In this you can see that we are prefix our service ids with the alias from the service loading class. The first two services defined are the response and request, these are just effectively mapping ids to class names. Those ids are then used in the service definition of the controller in the nested argument elements. These elements tell the container to pass the referenced service as constructor arguments when instantiating the controller.