-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
[DX] ADR usage #8153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[DX] ADR usage #8153
Changes from 27 commits
c8050b1
0d0d4cc
7359837
885e696
220021e
1fda263
11c77c7
d21feb2
a449220
6d1ed62
a81b625
8444e98
a93fa6c
97bcbac
72c2c74
666a89f
c8625fb
0d2022a
582f4bf
a237312
cd46501
24416f5
dc6ddc6
f1aad20
16c8472
eca9a0a
aa0a207
551aed6
3dacc68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
.. index:: | ||
single: Action Domain Responder approach | ||
|
||
How to implement the ADR pattern | ||
================================ | ||
|
||
In Symfony, you're used to implement the MVC pattern and extending the default :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` | ||
class. | ||
Since the 3.3 update, Symfony is capable of using natively the ADR approach. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it is worth it to add a link to https://github.com/pmjones/adr |
||
|
||
Update the configuration | ||
------------------------ | ||
|
||
The first step is to update the default services.yaml file, here's the new content: | ||
|
||
.. code-block:: yaml | ||
|
||
parameters: | ||
locale: 'en' | ||
|
||
services: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest to add xml and PHP versions too. |
||
_defaults: | ||
autowire: true | ||
autoconfigure: true | ||
public: false | ||
|
||
App\Actions\: | ||
resource: '../src/Actions' | ||
tags: | ||
- 'controller.service_arguments' | ||
|
||
Now that the container knows about our actions, time to build a simple Action ! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extra whitespace before the |
||
|
||
Updating your classes | ||
--------------------- | ||
|
||
As the framework evolve, you must update your classes, first, delete your Controller folder and create an Actions one then a new class using the ADR principles, for this example, call it ``HelloAction.php``: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo: |
||
|
||
.. code-block:: php | ||
|
||
<?php | ||
|
||
namespace App\Action; | ||
|
||
use App\Responders\HelloResponder; | ||
use Symfony\Component\HttpFoundation\Response; | ||
|
||
final class HelloAction | ||
{ | ||
public function __invoke(HelloResponder $responder): Response | ||
{ | ||
return $responder([ | ||
'text' => 'Hello World' | ||
]); | ||
} | ||
} | ||
|
||
.. tip:: | ||
|
||
As described in the DependencyInjection component documentation, you can still use the __construct() injection | ||
approach. | ||
|
||
By default, we define the class with the final keyword because this class shouldn't be extended, | ||
the logic is pretty simple to understand as you understand the ADR pattern, in fact, the 'Action' | ||
is linked to a single request and his dependencies are linked to this precise Action. | ||
|
||
.. tip:: | ||
|
||
By using the final approach and the private visibility (inside the container), our class | ||
is faster to return and easier to keep out of the framework logic. | ||
|
||
Once this is done, you can define the routes like before using multiples approaches: | ||
|
||
.. configuration-block:: | ||
|
||
.. code-block:: php-annotations | ||
|
||
# src/AppBundle/Action/HelloAction.php | ||
// ... | ||
|
||
/** | ||
* @Route("/hello", name="hello") | ||
*/ | ||
final class HelloAction | ||
{ | ||
// ... | ||
} | ||
|
||
.. code-block:: yaml | ||
|
||
# app/config/routing.yml | ||
hello: | ||
path: /hello | ||
defaults: { _controller: AppBundle\Action\HelloAction } | ||
|
||
.. code-block:: xml | ||
|
||
<!-- app/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="hello" path="/hello"> | ||
<default key="_controller">AppBundle\Action\HelloAction</default> | ||
</route> | ||
|
||
</routes> | ||
|
||
.. code-block:: php | ||
|
||
// app/config/routing.php | ||
use AppBundle\Action\HelloAction | ||
|
||
$collection->add('hello', new Route('/hello', array( | ||
'_controller' => HelloAction::class, | ||
))); | ||
|
||
Creating a Responder | ||
-------------------- | ||
|
||
As you can see in the __invoke call, this action require a ``HelloResponder`` class in order to build the response which is returned to the browser, first, update the services.yaml according to this need: | ||
|
||
.. code-block:: yaml | ||
|
||
parameters: | ||
locale: 'en' | ||
|
||
services: | ||
_defaults: | ||
autowire: true | ||
autoconfigure: true | ||
public: false | ||
|
||
App\Actions\: | ||
resource: '../src/Actions' | ||
tags: | ||
- 'controller.service_arguments' | ||
|
||
App\Responders\: | ||
resource: '../src/Responders' | ||
|
||
Here, the container only need to know about the existence of the classes, nothing difficult to understand as the fact that our Responders are responsable of returning the actual Response to the browser, no need to add the 'controller.service_arguments' tags as the Responders need to be called using the __invoke method in order to receive data from the Action. | ||
|
||
Now that the logic behind is clear, time to create the ``HelloResponder.php`` file: | ||
|
||
.. code-block:: php | ||
|
||
<?php | ||
|
||
namespace App\Responders; | ||
|
||
use Twig\Environment; | ||
use Symfony\Component\HttpFoundation\Response; | ||
|
||
final class HomeResponder | ||
{ | ||
private $twig; | ||
|
||
public function __construct(Environment $twig) | ||
{ | ||
$this->twig = $twig; | ||
} | ||
|
||
public function __invoke(array $data) | ||
{ | ||
return new Response( | ||
$this->twig->render('index.html.twig', $data) | ||
); | ||
} | ||
} | ||
|
||
If the routing is clearly define, the browser should display the traditional "Hello World" using the ADR approach, congrats ! | ||
|
||
Accessing the request | ||
--------------------- | ||
|
||
In many case, your classes can ask for any data passed via a form or via an API call, | ||
as you can imagine, as the logic evolve, your class is capable of accessing the request | ||
from a simple method injection like this: | ||
|
||
.. code-block:: php | ||
|
||
<?php | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
// ... | ||
|
||
public function __invoke(Environment $twig, Request $request): Response | ||
{ | ||
$id = $request->get('id'); | ||
|
||
return $twig->render('default/index.html.twig', array('id' => $id)); | ||
} | ||
} | ||
|
||
Final thought | ||
------------- | ||
|
||
Keep in mind that this approach can be completely different from what you're used to use, in order to | ||
keep your code clean and easy to maintain, we recommend to use this approach only if your code is | ||
decoupled from the internal framework logic (like with Clean Architecture approach) or if you start a new | ||
project and need to keep the logic linked to your business rules. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this last paragraph and the supposed benefits. I don't see how this approach or what you refer to the old one is cleaner or easier. I don't understand why this approach is more decoupled than the other one. I'm not saying this is good or bad, just that from a beginner's perspective, the benefits are not very clear. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,6 +80,11 @@ If your controller implements the ``__invoke()`` method - popular with the | |
Action-Domain-Response (ADR) pattern, you can simply refer to the service id | ||
(``AppBundle\Controller\HelloController`` or ``app.hello_controller`` for example). | ||
|
||
As this approach is evolving faster and can be easily transposed into Symfony, we recommend | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is it evolving? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this stage, not so much, like before, there's was an error in the real sense of my words, ADR still young and the "logic" keep evolving with time and usage by the developers, the true meaning was to say that using ADR still a "WIP" in Symfony and as the "logic" of ADR still evolving by the feedbacks of the developers, his implementation inside Symfony can be modified with future evolution of the framework or pattern. In fact, it was possible since Symfony 3.1 when recompiling the container (or like @dunglas do it, by injecting the classes as controllers and mapping the return of each one of them) but using it in production is way easier since 3.3 and 3.4 (and even easier in 4.0) |
||
you to read the following part: | ||
|
||
* :doc:`/controller/adr` | ||
|
||
Alternatives to base Controller Methods | ||
--------------------------------------- | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Symfony has never promoted the MVC pattern (at least, not since version 2). Instead, we are presenting Symfony as being a Request/Response framework where the controller converts a Request to a Response via a Controller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, my apologies for this words, the meaning of my "text" was to clearly say that a lot of developers use Symfony with MVC and that using this pattern is way more used than just transforming a Request into Response (which is handled by the framework in a certain way, the actual transformation occurs in the controller).