Get rid of annotations in your controllers!

Original author: Matthias Noback
  • Transfer
  • Tutorial
In the previous part of this series, we reduced the connectivity of the symphony controller and the framework, removing the dependency on the base class of the controller from FrameworkBundle. And in this part, we will get rid of some implicit dependencies that appear due to annotations.

Now let's look at the annotations. Initially, they were connected to speed up development (the need to edit the configuration file disappears, just solve problems right on the spot!):

namespace Matthias\ClientBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
/**
 * @Route("/client")
 */
class ClientController
{
    /**
     * @Route('/{id}')
     * @Method("GET")
     * @ParamConverter(name="client")
     * @Template
     */
    public function detailsAction(Client $client)
    {
        return array(
            'client' => $client
        );
    }
}


When you include these annotations, it detailsActionwill be executed when the URL matches the pattern /client/{id}. The parameter converter will receive the client entity from the database based on the parameter idthat will be extracted from the URL by the router. And the annotation @Templatewill indicate that the returned array is a set of variables for the template Resources/views/Client/Details.html.twig.

Excellent! And just a few lines of code. But all these automagic things imperceptibly connect our controller with a framework. Although there are no explicit dependencies, there are several implicit dependencies. The controller will only work if connected SensioFrameworkExtraBundlefor the following reasons:

1. It (SensioFrameworkExtraBundle) generates routing based on annotations
2. It takes care of turning the returned array into the correct objectResponse
3. He guesses which template should be applied
4. He turns a parameter idfrom a query into a real model

It would seem that this is not so scary, but SensioFrameworkExtraBundlea bundle, which means that it works only in the context of the Symfony 2 application. But we don’t we want to be tied to a specific framework (this is, in fact, the essence of this series of posts), so we need to get rid of this dependency.

Instead of annotations, we will use the usual configuration files and PHP code.

We use a router configuration

First of all, make sure that our routes are connected to Resources/config/routing.xml:

client_controller:detailsAction


You can use YAML, but lately something has hooked on XML.

Make sure that the service client_controlleractually exists, and do not forget to import the new one routing.xmlin the application settings, in the file app/config/routing.yml:

MatthiasClientBundle:
    resource: @MatthiasClientBundle/Resources/config/routing.xml


Now you can remove the annotations @Routeand @Methodfrom the controller class!

Create your own Response object yourself

Now, instead of hoping for annotation @Template, you can very well render the template yourself, and create a Response object containing the rendering result. You just need to inject the template engine into your controller, and specify the name of the template that you want to render:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;
class ClientController
{
    private $templating;
    public function __construct(EngineInterface $templating)
    {
        $this->templating = $templating;
    }
    /**
     * @ParamConverter(name="client")
     */
    public function detailsAction(Client $client)
    {
        return new Response(
            $this->templating->render(
                '@MatthiasClientBundle/Resources/views/Client/Details.html.twig',
                array(
                    'client' => $client
                )
            )
        );
    }
}

In the service declaration for this controller, you must also specify the service @templatingas an argument to the constructor:
services:
    client_controller:
        class: Matthias\ClientBundle\Controller\ClientController
        arguments:
            - @templating


After these changes, you can safely remove the annotation @Template

Get the required data yourself

And one more step to lower the connectivity of our controller. We are still dependent on SensioFrameworkExtraBundle, it automatically turns the parameter idfrom the request into real entities. This should not be difficult to fix, because we can just get the entity ourselves, using the entity repository directly:
...
use Doctrine\Common\Persistence\ObjectRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ClientController
{
    private $clientRepository;
    ...
    public function __construct(ObjectRepository $clientRepository, ...)
    {
        $this->clientRepository = $clientRepository;
        ...
    }
    public function detailsAction(Request $request)
    {
        $client = $this->clientRepository->find($request->attributes->get('id'));
        if (!($client instanceof Client) {
            throw new NotFoundHttpException();
        }
        return new Response(...);
    }
}

The service declaration should return the repository we need. We will achieve this in this way:
services:
    client_controller:
        class: Matthias\ClientBundle\Controller\ClientController
        arguments:
            - @templating
            - @client_repository
    client_repository:
        class: Doctrine\Common\Persistence\ObjectRepository
        factory_service: doctrine
        factory_method: getRepository
        public: false
        arguments:
            - "Matthias\ClientBundle\Entity\Client"

Finally, we got rid of annotations, which means that our controller can be used outside the Symfony 2 application (that is, one that does not depend on FrameworkBundleor on SensioFrameworkExtraBundle). All dependencies explicit, ie the controller to work, you need:

- HttpFoundation component (for classes Responseand NotFoundHttpException)
- template (for EngineInterface)
- an implementation of repositories Doctrine (Doctrine the ORM, Doctrine MongoDB ODM are, ...)
- the Twig templating for

left there is only one weak point: the names of our templates are still based on framework conventions (i.e. they use the bundle name as a namespace, e.g.@MatthiasClientBundle/...) This is an implicit framework dependency, as these namespaces are registered in the bootloader from the Twig file system. In the next post we will deal with this problem too.

Also popular now: