Prehľadnejšie controllery v Symfony

, Štítky: PHP, Symfony

V Symfony je filozofia „thin controllers, fat models“ – teda štíhly controller, tučný model. Inými slovami controller má byť len akousi prepojovacou vrstvou, ktorá obsahuje iba kód potrebný na riadenie jednotlivých častí aplikácie.

Ako docieliť aby obsahoval skutočne iba to potrebné a zároveň zostal prehľadný?

Zoberme ako príklad načítanie článku z databáze. Kód controlleru je vcelku jednoduchý a zrozumiteľný aj pre začiatočníka:

use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Response;
…
public function showAction($postId)
{
    $post = $this->getDoctrine()
        ->getRepository('AppBundle:Post')
        ->findOneById($postId);

    if (NULL === $post) {
        throw new NotFoundHttpException('Not found');
    }    

    $view = $this->renderView('blog/show.html.twig', [
        'post' => $post
    ]);

    return new Response($view);
}

A konfigurácia jeho routy:

# src/AppBundle/Resources/config/routing.yml
post.view:
    path:     /clanok/{postId}
    defaults: { _controller: AppBundle:Post:show }  

Metóde predáme argument $postId. Z kontajneru zoberieme Doctrine, načítame repozitár entity Post a pohľadáme jeden článok podľa id. Pokiaľ repozitár nič nenájde, vyhodíme výnimku NotFoundHttpException. Vykreslíme šablónu, predáme ju do objektu Response, ktorý vrátime (túto časť som schválne vypísal explicitne, aby bolo jasné čo controller vracia – v praxi sa používa return $this->render('…', []); čo je to isté ako posledných 5 riadkov kódu).

Tento kód má ale jeden problém – je dosť ukecaný.

Ide to aj jednodušie

Často sa opakujúce časti kódu môžeme zjednodušiť, alebo úplne vyhodiť (za použitia trochy mágie). K týmto účelom nám poslúži bundle od Sensio Labs, ktorý obsahuje pár praktických rozšírení konfigurovateľných cez anotácie.

Konfigurácia rout

V Best practices, ktoré som minule spomínal, je doporučené konfigurovať routy v controlleroch pomocou anotácií (a nielen routy). Je to praktické, pretože routu máme na očiach priamo pri práci s konkrétnou metódou. Osobne mi viac vyhovuje používať na konfiguráciu rout YAML, pretože sa mi tento formát lepšie číta a radšej pracujem s oddelenými konfiguračnými súbormi.

Výsledný zápis by vyzeral takto:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
…
/**
 * @Route("/clanok/{postId}", name="post.view")
 */
public function showAction($postId)
…  

Pri použití anotácie @Route nám teda odpadol jeden konfiguračný súbor (je potreba upraviť app/config/routing.yml). Zvyšok kódu zostáva rovnaký.

Načítavanie entít

V kóde controlleru je veľmi častý mechanizmus, ktorý načíta entitu podľa nejakých kritérií a v prípade, že dané kritériá žiadna nespĺňa, vyhodí výnimku. Presne o toto sa stará anotácia @ParamConverter.

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use AppBundle\Entity\Post;
…
/**
 * @Route("/clanok/{postId}", name="post.view")
 * @ParamConverter("post", class="AppBundle:Post")
 */
public function showAction(Post $post)
{
    $view = $this->renderView('blog/show.html.twig', [
        'post' => $post
    ]);

    return new Response($view);
}

V tomto prípade použije ParamConverter jeden z východzých konvertorov – DoctrineParamConverter –, aby načítal entitu. Kritériá podľa ktorých bude hľadať, si zistí z konfigurácie routy.

Pri použití anotácie @ParamConverter nám odpadla práca s repozitárom, načítanie entity a vyhodenie výnimky. Navyše metóda dostala type hint a tak je jasnejšia jej závislosť.

Konfigurácia šablón

Bežná metóda controlleru vykresľuje nejakú šablónu. Tú potom vloží do objektu Response a ten vráti. Môžeme to spraviť v dvoch krokoch ako doteraz, ale častejšie sa používa metóda render. Celé to ale môžeme ešte zjednodušiť pomocou anotácie @Template.

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use AppBundle\Entity\Post;
…
/**
 * @Route("/clanok/{postId}", name="post.view")
 * @ParamConverter("post", class="AppBundle:Post") 
 * @Template("blog/show.html.twig")
 */
public function showAction(Post $post)
{
    return [
        'post' => $post
    ];
}

V anotácii @Template nastavíme adresu šablóny, ktorá sa vykreslí a už nevraciame spomínanú metódu render, ale iba pole parametrov, ktorá sa automaticky predajú šabĺónovacej vrstve.

Celkom prehľadné, nie? Takto to, ale používa málokto. Častejšie použitie je toto:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use AppBundle\Entity\Post;
…
/**
 * @Route("/clanok/{postId}", name="post.view")
 * @Template("blog/show.html.twig")
 */
public function showAction(Post $post)
{}

Funguje to úplne rovnako. To je tá v úvode spomínaná mágia. Pokiaľ so Symfony pracujete nejakú dobu, môže vám to pripadať ako samozrejmosť. Nevýhodou, ale je, že keď sa na kód pozrie niekto nový, nebude mu jasné čo načíta entitu, ani odkiaľ a ako. Ak používate pri vývoji IDE, tak vás bude upozorňovať na to, že premenná $post sa nepoužíva, atď.. Použitie tejto konfiguračnej direktívy stojí za zváženie.

Výsledok

Keďže SensioFrameworkExtraBundle vo východzom stave automaticky konvertuje type hint argumenty, celý kód z úvodu môžeme teda zapísať – bez toho aby stratil na prehľadnosti –, takto:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use AppBundle\Entity\Post;
…
/**
 * @Route("/clanok/{postId}", name="post.view")
 */
public function showAction(Post $post)
{
    return $this->render('blog/show.html.twig', [
        'post' => $post
    ]);
}

← Späť na zoznam