
Framework-independent controllers. Finishing touches
- Transfer
Thanks! And let me explain.
First of all, thanks to everyone who read the previous parts . I wrote a lot of interesting comments, and I realized that I had to explain why, in fact, am I writing all this? Why do I even need to separate the controllers from the framework? Most likely, you don’t have to think about it, because
Well said, and I agree with that. You do not need to do this, because no one usually changes frameworks. Although it is possible that someone will want to use your code in an application on a different framework. But:
Indeed, in the controllers there should be almost no code, only calls to services, receiving data from them and returning any answer. They can even be rewritten to another framework. And if you are not going to distribute your code, then again, it makes no sense to separate the controllers from the framework.
But when you do this, you will become a little happier. Not only your controllers will become more independent, you will become independent as a developer. You no longer have 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:
And that is why I am writing this series of posts. They give developers a better understanding of what is really going on inside the framework, and how it helps them. Developers are becoming more confident, more independent. They cease to be “symphony developers”, they become more skilled developers in the general sense. This series of articles is an excellent exercise in detecting addictions. The agreements in the controllers, by the way, are also dependencies: even if they are hidden from our eyes, we have the opportunity to train our "connected radars."
Let's take the last few steps to the framework-independent controllers. In the previous part, we removed all annotations and used configuration files instead. We also introduced several dependencies that allowed us to get data from the database and render the template. But the template name still contained the name of the bundle as a namespace:
Since we decided to make the controller work in applications where there are no bundles will have to choose a more generic name, for example
When you add this path to
We didn’t get a very elegant solution, because we need to edit the config the first time we connect
Now you can remove the extra line from
Getting Rid of HttpFoundation Dependence
The previous article raised doubts among some readers:
In my opinion, being dependent on Doctrine is not so bad. This is just a library that I liked (like Twig). Detaching from ORM / ODM is also an interesting thing, and it can be implemented, but it is still outside the scope of this series of posts.
But Jerry is right: removing annotations, we bring class dependencies to
However, we can take another step and stop depending on HttpFoundation. We need to get rid of objects
No class from HttpFoundation is now mentioned here! Now you can add a wrapper controller that would bind our controller with this component:
Yes, the code is becoming a little more difficult to understand. So I do not recommend you to do so. But still I would like to show you that this is really possible.
Last topic for discussion: controller action. The de facto standard is that related actions are packaged into one controller class. For example, all actions with clients must be in the class
The solution to this problem is actually very simple, and significantly simplifies the perception of the controller code. And the controllers are becoming easier to look for. It’s just that actions need to be grouped a little differently. By and large, each action games released in a separate class, and the directory in which turns out to be an action-controller becomes the link, here's an example:
I first learned about this technique in Paul Jones' book Modernizing Legacy Applications in PHP. I have used it several times already, and I must say that in some cases it has become much more pleasant to work with controllers!
Well, I gave you some ideas the next time you create another controller. I also hope that you are now better acquainted with the framework itself. And I also hope that your mind is free :)
First of all, thanks to everyone who read the previous parts . I wrote a lot of interesting comments, and I realized that I had to explain why, in fact, am I writing all this? Why do I even need to separate the controllers from the framework? Most likely, you don’t have to think about it, 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 no one usually changes frameworks. Although it is possible that someone will want to use your code in an application on a different 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, in the controllers there should be almost no code, only calls to services, receiving data from them and returning any answer. They can even be rewritten to another framework. And if you are not going to distribute your code, then again, it makes no sense to separate the controllers from the framework.
But when you do this, you will become a little happier. Not only your controllers will become more independent, you will become independent as a developer. You no longer have 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 is more useful than before, and because of this, I think more about the very essence of the code (Kevin Bond)
And that is why I am writing this series of posts. They give developers a better understanding of what is really going on inside the framework, and how it helps them. Developers are becoming more confident, more independent. They cease to be “symphony developers”, they become more skilled developers in the general sense. This series of articles is an excellent exercise in detecting addictions. The agreements in the controllers, by the way, are also dependencies: even if they are hidden from our eyes, we have the opportunity to train our "connected radars."
Twig Templates
Let's take the last few steps to the framework-independent controllers. In the previous part, we removed all annotations and used configuration files instead. We also introduced several dependencies that allowed us to get 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 the controller work in applications where there are no bundles will have to choose a more generic 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('/путь/до/шаблонов', 'ПространствоИменДляШаблонов')
. What is good is that in a Symfony 2 application this can be done by specifying a configuration parameter twig.paths
:# в config.yml:
twig:
paths:
"%kernel.root_dir%/../src/Matthias/Client/View": "MatthiasClient"
When you add this path to
config.yml
, the code in the controller can be changed in this way:return new Response(
$this->templating->render(
'@MatthiasClient/Client/Details.html.twig',
...
)
);
Even better: pre-wiring configuration
We didn’t get a very elegant solution, because we need to edit the config the first time we 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. It is necessary for the extension to implement PrependExtensionInterface
and provide an array of values that must be added before the main 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 Dependence
The previous article raised doubts among some readers:
And now the controller is clearly dependent on Doctrine and HttpFoundation! In the version with annotations, this was not! (Jerry Vandermeisen)
In my opinion, being dependent on Doctrine is not so bad. This is just a library that I liked (like Twig). Detaching from ORM / ODM is also an interesting thing, and it can be implemented, but it is still outside the scope of this series of posts.
But Jerry is right: removing annotations, we bring class dependencies to
Request
and Response
from the HttpFoundation component. However, unlike him, I believe that this is already a good step towards decoupling from the framework, since more and more frameworks besides Symfony support HttpFoundation as an abstraction layer for HTTP. However, we can take another step and stop depending on HttpFoundation. We need to get rid of objects
Request
and Response
. 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
);
}
No class from HttpFoundation is now mentioned here! Now you can add a wrapper controller that would bind 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 is becoming a little more difficult to understand. So I do not recommend you to do so. But still I would like to show you that this is really possible.
Get rid of the "action" methods
Last topic for discussion: controller action. The de facto standard is that related actions are packaged into one controller class. For example, all actions with clients must be in the class
ClientController
. That is, it has methods like newAction
, editAction
etc. And if you use dependency injection in the constructor, it is quite possible that some of them will not even be used in some action games. The solution to this problem is actually very simple, and significantly simplifies the perception of the controller code. And the controllers are becoming easier to look for. It’s just that actions need to be grouped a little differently. By and large, each action games released in a separate class, and the directory in which turns out to be an action-controller becomes the link, here's an example:
Controller\Client\New
,Controller\Client\Edit
, etc. Each of these classes will have one public method called when the controller executes. 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 Paul Jones' book Modernizing Legacy Applications in PHP. I have used 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 some ideas the next time you create another controller. I also hope that you are now better acquainted with the framework itself. And I also hope that your mind is free :)