Symfony 4: Vytváříme chytrý kontroler
Vladimír MacháčekCo kdyby byl Symfony kontroler schopný automaticky najít správnou šablonu k požadované akci bez nutnosti opakovaně psát její cestu? Co takhle mít možnost zasílat parametry do šablony z více míst a třeba i před renderovací metodou? Symfony 4 je skvělý framework ale po chvíli práce s ním mi začaly chybět některé fičury, na které jsem byl zvyklý z jiných frameworků, jako je například Nette Framework. Rozhodl jsem se, že si je do Symfony musím dodělat. V tomto článku vám ukážu, jak jsem toho docílil.
Řekněme, že máme nějaký HomepageController
s renderDefault()
metodou umístěný ve složce src/controller
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class HomepageController extends AbstractController
{
/**
* @Route(path="/", name="homepage")
*/
public function renderDefault(): Response
{
$number = mt_rand(0, 100);
return $this->render('default.twig', [
'number' => $number,
]);
}
}
a default.twig šablonu pro renderDefault akci ve složce templates.
Number: {{ number }}
Všechno funguje a vypadá v pořádku. No jo, jenomže co když budu náhodou potřebovat vložit parametr odjinud
než z renderDefault()
metody? To je v tuto chvíli nemožné..., ledaže bychom si vytvořili AbstractController
, který nám to umožní.
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
abstract class AbstractCustomController extends AbstractController
{
/**
* @var mixed[]
*/
private $templateParameters = [];
protected function setTemplateParameters(array $parameters): void
{
$this->templateParameters = array_merge($this->templateParameters, $parameters);
}
protected function renderTemplate(string $template, array $parameters = [], Response $response = null): Response
{
$this->setTemplateParameters($parameters);
return $this->render(
$template, $this->templateParameters, $response
);
}
}
Zbývá už jen AbstractController
podědit v HomepageController
, vytvořit setter metodu a zavolat ji v renderDefault()
metodě.
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class HomepageController extends AbstractController
{
/**
* @Route(path="", name="homepage")
*/
public function renderDefault(): Response
{
$this->setRandomNumberIntoTemplate();
return $this->renderTemplate('default.twig');
}
private function setRandomNumberIntoTemplate(): void
{
$number = mt_rand(0, 100);
$this->setTemplateParameters([
'number' => $number
]);
}
}
Takto vytvořená metoda pro předávání parametrů do šablony je sice pěkná, ale kdybychom chtěli dané číslo vkládat
do každé šablony automaticky, je potřeba ji neustále a dokola volat v každé render metodě. V tuhle chvíli
by se hodila beforeRender()
metoda, tak si ji pojďme přidat do AbstractControlleru
.
// ...
protected function beforeRender(): void {}
// ...
protected function renderTemplate(string $template, array $parameters = [], Response $response = null): Response
{
$this->beforeRender();
$this->setTemplateParameters($parameters);
return $this->render(
$template, $this->templateParameters, $response
);
}
Nyní jen stačí tuto metodu použít v HomepageController
.
// ...
public function beforeRender(): void
{
$this->setRandomNumberIntoTemplate();
}
// ...
/**
* @Route(path="/", name="homepage")
*/
public function renderDefault(): Response
{
return $this->renderTemplate('default.twig');
}
V HomepageController
je ale stále potřeba zapisovat cestu k šabloně. Většinou preferuji modulární strukturu aplikace
s šablonami umístěnými ve složce pojmenované po kontroleru zanořené v templates složce, která je ve stejné složce jako kontrolery.
Zní to trošku divně, takže uvedu jednoduchý příklad:
HomepageController
=>src/Modules/HomepageModule/Controller/HomepageController.php
- Šablona =>
src/Modules/HomepageModule/Controller/templates/Homepage/default.twig
Povětšinou ještě modul dělím na admin a front ale pro tento článek je tato struktura dostačující. V dalším kroku je tedy potřeba
přesunout Homepage modul a jeho šablony do zmiňované adresářové struktury, a stejně tak přesunout AbstractController
. Ten však bude
například ve složce CoreModule src/Modules/CoreModule/Controller/AbstractController.php
.
Abychom to všechno zprovoznili, je potřeba provést několik úprav. Nejdříve upravíme AbstractController
,
protože zde nastává největší změna.
namespace App\Modules\CoreModule\Controller;
// ...
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as SymfonyAbstractController;
abstract class AbstractController extends SymfonyAbstractController
{
// ...
protected function renderTemplate(array $parameters = [], Response $response = null): Response
{
preg_match(
'/\:\:render(?<template>\S+)/',
$this->get('request_stack')->getCurrentRequest()->attributes->get('_controller'),
$matches
);
// ...
return $this->render(
$this->getTemplatePath(strtolower($matches['template'])),
// ...
);
}
public function getTemplatePath(string $view): string
{
$reflector = new \ReflectionClass(get_called_class());
$templatesDirectoryName = str_replace(
'Controller',
'',
basename($reflector->getFileName(), '.php')
);
$moduleTemplatesDirectoryPath = str_replace (
$this->getParameter('kernel.root_dir') . '/',
'',
dirname($reflector->getFileName())
). '/templates/' . $templatesDirectoryName;
return $moduleTemplatesDirectoryPath . '/' . $view . '.twig';
}
}
Přibylo volání preg_match
funkce v renderTemplate()
metodě, a byla přidána metoda getTemplatePath()
.
Tato metoda roztokenuje jméno aktuálního kontroleru a render metody, a následně vrátí cestu k šabloně.
Za další je potřeba upravit HomepageController
. Zde již není cesta k šabloně, protože ji nepotřebujeme.
/**
* @Route(path="/", name="homepage")
*/
public function renderDefault(): Response
{
return $this->renderTemplate();
}
Nesmíme zapomenout nakonfigurovat Twig a anotace.
# twig.yml
twig:
paths: ['%kernel.project_dir%/src']
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
# annotations.yml
controllers:
resource: ../../src/Modules/
type: annotation
Poslední co je potřeba upravit je cesta pro mapování kontrolerů.
App\Modules\:
resource: '../src/Modules'
tags: ['controller.service_arguments']
Hotovo! Nyní už nemusíme psát cestu k šabloně, můžeme předávat parametry do šablon z více míst a popřípadě je vkládat automaticky v beforeRender()
metodě.
Nevýhodou toho všeho je, že je potřeba dodržovat adresářovou strukturu, která je nastavena v getTemplatePath()
metodě ve třídě AbstractController
.
Budu rád za jakýkoliv váš feedback (klidně i negativní)!
Originálně publikováno na https://machy8.com/blog/symfony-4-creating-smart-controller