Récemment, j’ai été confronté au fait de devoir manipuler les mêmes données pour ensuite les convertir dans trois formats différents (des formats de sous-titres en l’occurrence).
J’ai aussi eu besoin de manipuler les mêmes données pour les renvoyer en html ou en json.
Le design pattern Builder est parfait pour ça. Si on ne l’applique pas, voici ce que cela pourrait donner:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/** * @param Request $request * * @Route( * "/monitoring/status.{_format}", * methods={"GET"}, * defaults={"_format": "html"}, * requirements={ * "_format": "html|json" * } * ) * * @return JsonResponse|Response * * @throws \Exception */ public function renderAction(Request $request) { // bien sûr, ici, c'est un service qui va aller chercher ces informations et les transformer en tableau $monitoring = [ ['name' => "connection mysql", "status" => 1, "message" => "The mysql connection succeeded."], ['name' => "connection mongoDB", "status" => 0, "message" => "The mongodb connection failed."], ]; if ('json' === $request->getRequestFormat()) { return new JsonResponse($monitoring); } return $this->render('index.html.twig', ['results' => $monitoring]); } |
Ok, vous allez me dire: « oh, ce n’est pas dramatique! » Oui, c’est sûr. Mais imaginez que l’on doive finalement avoir un troisième rendu en csv… On revient sur le code (donc on ne respecte pas le O de SOLID) et on rajoute un if. Et en plus, comme le rendu est plus complexe, on appelle une classe qui va effectuer le rendu csv.
Et imaginez qu’il y a un quatrième rendu… puis un cinquième… et que chacun fait appel à sa propre classe de rendu (par exemple sous forme de camembert, que sais-je!). Ou qu’un jour, on décide de ne pas simplement renvoyer du json mais d’avoir un rendu plus élaboré (et donc d’appeler une classe qui va le rendre)… vous commencez à voir le souci.
Ce que je vous propose ici est un code simple mais qui va découpler les builders et rendre le contrôleur complètement dynamique sans que l’on ait un jour besoin d’y revenir.
Construire nos builders
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?php namespace App\Builder; use Symfony\Component\HttpFoundation\JsonResponse; class JsonBuilder implements StatusBuilderInterface { /** * @param array $results * * @return JsonResponse|\Symfony\Component\HttpFoundation\Response */ public function getReport(array $results) { return new JsonResponse($results); } /** * @param string $format * * @return StatusBuilderInterface|string */ public function isFormatMatch(string $format) { return 'json' === $format; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?php namespace App\Builder; use Symfony\Component\HttpFoundation\Response; class HtmlBuilder implements StatusBuilderInterface { /** * @var \Twig_Environment */ private $templating; /** * @param \Twig_Environment $templating */ public function __construct(\Twig_Environment $templating) { $this->templating = $templating; } /** * @param array $results * * @return Response * * @throws \Twig_Error_Loader * @throws \Twig_Error_Runtime * @throws \Twig_Error_Syntax */ public function getReport(array $results) { return new Response($this->templating->render('index.html.twig', ['results' => $results])); } /** * @param string $format * * @return StatusBuilderInterface|string */ public function isFormatMatch(string $format) { return 'html' === $format; } } |
Ne vous étonnez pas de la fonction isFormatMatch, elle va nous servir plus tard pour déterminer à la volée le bon builder dans le contrôleur.
Notre BuilderResolver
Il est temps de déterminer la classe qui aura dans un tableau les différents builders, rajoutés à la volée par le CompilerPass, et qui retournera le builder voulu par rapport au format attendu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<?php namespace App\Builder; class BuilderResolver { /** * @var array */ private $builders = []; /** * @param StatusBuilderInterface $builder */ public function addBuilder(StatusBuilderInterface $builder) { array_push($this->builders, $builder); } /** * @param string $format * * @return StatusBuilderInterface * * @throws \Exception */ public function getBuilder(string $format) : StatusBuilderInterface { /** * @var StatusBuilderInterface $builder */ foreach ($this->builders as $builder) { if ($builder->isFormatMatch($format)) { return $builder; } } throw new \Exception(sprintf('No builder could be found with format %s.', $format)); } } |
Vous allez peut-être me dire: « mais comment les builders vont-ils être ajoutés à la volée? ». Pour faire cela, il faut déjà tagguer nos builders qui implémente une interface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php namespace App\Builder; use Symfony\Component\HttpFoundation\Response; interface StatusBuilderInterface { /** * @param array $results * * @return Response */ public function getReport(array $results); /** * @param string $format * * @return StatusBuilderInterface */ public function isFormatMatch(string $format); } |
Ensuite, dans services.yaml (Symfony 3.4/4.1):
1 2 3 4 5 |
services: _instanceof: App\Builder\StatusBuilderInterface: tags: - { name: app.status_builder } |
Puis le compilerPass:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?php namespace App\DependencyInjection\Compiler; use App\Builder\BuilderResolver; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Reference; class StatusBuilderCompilerPass implements CompilerPassInterface { /** * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { if (!$container->has(BuilderResolver::class)) { throw new ServiceNotFoundException('BuilderResolver not found (BuilderResolver::class).'); } $builderResolver = $container->getDefinition(BuilderResolver::class); foreach ($container->findTaggedServiceIds('app.status_builder') as $id => $tags) { $builderResolver->addMethodCall('addBuilder', [new Reference($id)]); } } } |
et sa déclaration dans Kernel.php:
1 2 3 4 5 6 |
public function build(ContainerBuilder $container) { parent::build($container); $container->addCompilerPass(new StatusBuilderCompilerPass()); } |
Le contrôleur
Il va appeler le BuilderResolver et lui passer en argument le format demandé dans l’uri:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<?php namespace App\Controller; use App\Builder\BuilderResolver; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; class StatusReportController extends Controller { /** * @param Request $request * @param BuilderResolver $builderResolver * * @Route( * "/monitoring/status.{_format}", * methods={"GET"}, * defaults={"_format": "html"}, * requirements={ * "_format": "html|json" * } * ) * * @return JsonResponse|Response * * @throws \Exception */ public function renderAction(Request $request, BuilderResolver $builderResolver) { // bien sûr, ici, c'est un service qui va aller chercher ces informations et les transformer en tableau $monitoring = [ ['name' => "connection mysql", "status" => 1, "message" => "The mysql connection succeeded."], ['name' => "connection mongoDB", "status" => 0, "message" => "The mongodb connection failed."], ]; $builder = $builderResolver->getBuilder($request->getRequestFormat()); return $builder->getReport($monitoring); } } |
Le mot de la fin
Je sais que ça a l’air plus lourd ainsi et que l’on se dit: « mais pourquoi se compliquer autant la vie? » Mais par expérience, dès qu’il commence à y avoir deux cas, il y en aura un jour un troisième, et un quatrième etc. Avec ce design pattern, vous vous assurez de ne pas modifier le code au niveau du contrôleur (à part au niveau de l’annotation pour rajouter de nouveaux formats).
Vous avez juste à créer de nouveaux builders, tous indépendants les uns des autres, et qui s’enregistreront automatiquement dans le BuilderResolver sans que vous n’ayez rien à faire.
Le code est visible sur github ici: https://github.com/jpsymfony/dp-builder
Cet article s’inspire de celui-ci: http://www.croes.org/gerald/blog/le-design-pattern-monteur-builder-en-php/687/
2 comments
Merci j’ai appris un truc en lisant ce post
Merci!