📜 ⬆️ ⬇️

Get rid of annotations in your controllers!

In the previous part of this series, we lowered the connectedness of the symphony controller and framework by removing the dependency on the base controller class from the 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 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, detailsAction will be executed when the URL matches the /client/{id} template. The parameter converter will receive the client's entity from the database based on the id parameter that will be extracted from the router's URL. And the @Template annotation indicates that the returned array is a set of variables for the Resources/views/Client/Details.html.twig .
')
Fine! And just a few lines of code. But all these automagic things implicitly connect our controller with the framework. Suppose there are no explicit dependencies, there are several implicit dependencies. The controller will work only when SensioFrameworkExtraBundle connected for the following reasons:

1. It (SensioFrameworkExtraBundle) generates routing based on annotations
2. It takes care of turning the returned array into the correct Response object.
3. He guesses which pattern to apply.
4. It turns the id parameter from the query into a real model.

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

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

We use the configuration of the router

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

 <?xml version="1.0" encoding="UTF-8" ?> <routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> <route id="client.details" path="/client/{id}" methods="GET"> <default key="_controller">client_controller:detailsAction</default> </route> </routes> 


You can use YAML, but I recently got hooked on XML.

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

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


Now you can remove the @Route and @Method from the controller class!

Create a Response object yourself

Now, instead of hoping for the @Template annotation, you can easily render the template yourself, and create a Response object containing the result of the rendering. You just need to inject the template engine into your controller, and specify the name of the template 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 also need to specify the @templating service as a constructor argument:
 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 id parameter from the query into real entities. This should be easy to fix, because we can simply 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 announcement 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 the annotations, which means that our controller can be used outside of the Symfony 2 application (that is, one that does not depend on either the FrameworkBundle or SensioFrameworkExtraBundle ). All dependencies are obvious, that is, in order for the controller to work, you need:

- HttpFoundation component (for Response and NotFoundHttpException )
- template engine (for EngineInterface )
- any implementation of Doctrine repositories (Doctrine ORM, Doctrine MongoDB ODM, ...)
- Twig for templating

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

Source: https://habr.com/ru/post/227787/


All Articles