📜 ⬆️ ⬇️

Framework-independent controllers. Finishing touches

Thank! And let me explain.

First of all, thanks to everyone who read the previous parts . Many interesting comments have been written, and I realized that I must explain why I actually write all this? Why do I need to separate the controllers from the framework? Most likely, this will not have to think, because

The chances that the controllers will have to be transferred to another framework are close to zero. (Rafael Doms)


Well said, and I agree with that. You do not need to do this, because nobody usually changes frameworks. Although it is possible that someone will want to use your code in the application on another framework. But:
')
If you have a “thin” controller, in which everything depends on the service layer, then it will be elementary to rewrite it to another framework (Rafael Doms)


Indeed, there should be almost no code in the controllers, only calls to services, receiving data from them and returning any response. They can even be rewritten to another framework not needed. And if you are not going to distribute your code, then again there is no point in separating controllers from the framework.

But when you do this, you will become a little happier. Not only your controllers will become more independent, you as an developer will also become independent. No longer need to rely on helper functions, annotations, or magic: you can do everything yourself. I really like the following quote, because it accurately describes my impressions:

Now, writing a new controller brings more benefits than before, and because of this, I think more about the essence of the code (Kevin Bond)


And this is why I am writing this series of posts. They give developers a better understanding of what is actually happening inside the framework, and how it helps them. Developers are becoming more confident, more independent. They cease to be "symphony developers", they become more skillful developers in a general sense. This series of articles is an excellent exercise in identifying dependencies. Agreements in the controllers, by the way, are also dependencies: let them be hidden from our eyes, but we have the opportunity to train our "radar of connections".

Twig Templates


Let's take the last few steps to the framework-independent controllers. In the previous section, we removed all the annotations and used configuration files instead. We also implemented several dependencies that allowed us to obtain data from the database and render the template. But the template name still contained the name of the bundle as a namespace:

class ClientController { ... public function detailsAction(Client $client) { return new Response( $this->templating->render( '@MatthiasClientBundle/Resources/views/Client/Details.html.twig', ... ) ); } } 

Since we decided to make this controller work in applications where there are no bundles, we will have to choose a more general name, for example, MatthiasClient . Now you need to register this name as a namespace for Twig templates. To do this, call the method Twig_Loader_Filesystem::addPath('///', '') For Twig_Loader_Filesystem::addPath('///', '') . What is good is that in an application on symfony 2 this can be done by specifying the twig.paths configuration twig.paths :

 #  config.yml: twig: paths: "%kernel.root_dir%/../src/Matthias/Client/View": "MatthiasClient" 


When you add this path in config.yml , the code in the controller can be changed like this:

 return new Response( $this->templating->render( '@MatthiasClient/Client/Details.html.twig', ... ) ); 


Better yet: preconnect configuration


We got a not very elegant solution, because you need to edit the config when you first connect MatthiasClientBundle to the project. There is a better option: you can programmatically add values ​​to the configuration from the extension class of your bundle. The extension must implement PrependExtensionInterface and provide an array of values ​​that must be added before the basic values ​​in config.yml :

 use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; class MatthiasClientExtension extends Extension implements PrependExtensionInterface { ... public function prepend(ContainerBuilder $container) { $container->prependExtensionConfig( 'twig', array( 'paths' => array( '%kernel.root_dir%/../src/Matthias/Client/View' => 'MatthiasClient' ) ) ); } } 


Now you can remove the extra line from config.yml , because now this value is added automatically.

Getting rid of HttpFoundation dependency

The previous article raised doubts among some readers:

And now the controller clearly depends on Doctrine and HttpFoundation! In the version with annotations this was not! (Jerry Vandermazen)


In my opinion, depend on Doctrine is not so bad. This is just a library I liked (like Twig). Unbinding from ORM / ODM is also an interesting thing, and quite realizable, but it is still outside of this series of posts.

But Jerry is right: removing annotations, we add dependency on the Request and Response classes from the HttpFoundation component. However, unlike him, I believe that this is already a good step towards unlinking from the framework, since more and more other frameworks besides Symfony support HttpFoundation as a layer of abstraction for HTTP.

However, we can take another step and stop depending on HttpFoundation. We need to get rid of the Request and Response objects. Let's change the controller like this:

 public function detailsAction($id) { $client = $this->clientRepository->find($id); if (!($client instanceof Client)) { return array(null, 404); } return array( $this->templating->render( '@MatthiasClient/Client/Details.html.twig', array('client' => $client) ), 200 ); } 


Not a single class from HttpFoundation is now mentioned here! Now you can add a controller wrapper that would connect our controller with this component:

 use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class SymfonyClientController { private $clientController; public function __construct(ClientController $clientController) { $this->clientController = $clientController; } public function detailsAction(Request $request) { $result = $this->clientController->detailsAction($request->attributes->get('id')); list($content, $status) = $result; if ($status === 404) { throw new NotFoundHttpException($content); } return new Response($content, $status); } } 


Yes, the code becomes somewhat more difficult to understand. So I do not recommend you do that. But still I would like to show you that this is indeed possible.

Get rid of the "action" -methods


The last topic for discussion: action controllers. The de facto standard is that the associated actions are packaged into one controller class. For example, all client actions must be in the ClientController class. That is, it has methods like newAction , editAction , etc. And if you use dependency injection in the constructor, then it is quite possible that some of them will not even be used in some actions.

The solution to this problem is actually very simple, and significantly simplifies the perception of the controller code. It also makes it easier to search for controllers. Just need to group actions a little differently. By and large, each of the actions is allocated in a separate class, and the directory in which such an action controller finds itself becomes a link, here's an example: Controller\Client\New , Controller\Client\Edit , and so on. Each of these classes will have one public method called when the controller is executed. Let's call it __invoke :

 namespace Matthias\Client\Controller\Client; class Details { public function __construct(...) { //          } public function __invoke(Client $client) { ... } 


I first learned about this technique in the book of Paul Jones Modernizing Legacy Applications in PHP. I have applied it several times already, and I must say that in some cases it has become much more pleasant to work with controllers!

Conclusion

Well, I gave you a few ideas the next time you create another controller. I also hope that you are now better acquainted with the framework itself. And I hope that your mind is free :)

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


All Articles