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.

You can leave a response, or trackback from your own site.

9 Responses to “Symfony2: Controller as Service”

  1. Stephan says:

    Thanks- great posting!

    DO you have any idea how I could use that controller as base controller?
    I guess when I try to extend it (via “MyController extends StaticController”), the dependency injection stuff won’t work- right?

    BR Stephan

  2. miller says:

    If your controller does not override the constructor then it would still be called, you would need to add a service with that class to the XML config file with the new class name.

    If your class had more dependencies then you could override the constructor, have the request and response as arguments and call the parent constructor passing them as arguments.

    If you need to pass the same dependencies to a lot of controllers though you may be better looking at interface injection rather than extending this class. I have written about it in this post – http://miller.limethinking.co.uk/2011/04/18/symfony2-dependency-injection-types/

  3. Stephan says:

    Thanks for the link. Indeed a great article about DI- the code snippets are extremely useful! However I did not manage to get it running as I need it.
    The problem with interface injection is: I have to implement each setter in each controller.
    I tried to make a basecontroller and use the container injection. problem is: the subcontroller (the one which inherits the basecontroller) doesn’t get the container argument pushed to the constructor from the DI. I guess thats because just the basecontroller is defined as service, not the subcontroller.
    I dev. don’t want to configure a service for EACH controller which inherits the basecontroller- is this possible?

    BR Stephan

  4. cordoval says:

    I think you miss one step here,

    namely:

    $manager = $this->container->get(‘lime_thinking_spring.static’);

    am I right? why would you want to inject it through the controller? oh now I see it is because you are not extending ContainerAware …

  5. miller says:

    @Stephan Sorry for the delay in responding, I have been on holiday. If you use setter injection you can just have the setter method in the base controller that you are extending, then would be no need to define it in each class.

    Interface injection has been removed from symfony2 so you would now need to define passing the dependency to the setter method for each service in the XML config. I am not sure that there is anyway to avoid this.

  6. miller says:

    @cordoval Thanks for your comment, I could get my service through your code if need be, the code shown though is the internals of that service rather than how it is instantiated itself. That could be done the way you suggest or it could be routed to as shown above.

    Implementing the ContainerAware interface would avoid the step of injecting the container, although I would still recommend avoiding container injection altogether anyway.

  7. Web Hosting says:

    Is it possible to use the service configuration file (service.xml) just to store default configuration parameters so that if these are not configured via the config.yml config file these defaults to my values from the service file?

Leave a Reply

Please use [code] and [/code] around any source code/html/xml you wish to include in a comment.

Subscribe to RSS Feed Follow me on Twitter!