Videa Blog

Symfony 4: Vytváříme chytrý kontroler

Vladimír Macháček  

Co 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:

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