PHP: Routing podle anotací

Publikováno PHP

Po předchozím komponentách (anotacedependecy injection container a další) přichází konečně něco praktického :). Zapojíme vše dohromady a připojíme novou komponentu na routování (směrování).

Problém

Co by měl router dělat? Máme request (dotaz) a chceme response (odpověď). Router má být ten most mezi tím. A teď nastává otázka: Jak mu říct, kterou odpověď přesně má na daný dotaz vrátit?

Řešení

Moc se mi líbí řešení, které nabízí Symfony, a to konkrétně anotace. To, že u metody v controlleru definuji, jak má vypadat routa (cesta) k ní mi přijde vhodnější (ne vždy, ale častěji ano) než konfigurace mimo. Při vhodné konvenci, možností si vypsat všechny definované cesty a podchycení testy se nestane, že byste se ztratili v tom, kde máte co definováno a jestli se to nebije mezi sebou.

Vlastní komponenta

Ve své komponentě používám routování podle anotací jako primární zdroj rout s tím, že v konfiguračním souboru definuji složku, ve které má proběhnout analýza controllerů.

Komponenta je závislá prakticky na všech předchozích, které jsem vytvářel. Celý kód už je trochu rozsáhlejší, takže zde uvedu jak komponentu použít a na zdrojový kód se můžete mrknut na github.com/gephart/routing

.htaccess

V první řadě je potřeba přesměrovat požadavky na server na hlavní soubor, ze kterého budeme pracovat, což je v tomto případě index.php

RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php?_route=$1 [L,QSA]

index.php

Náš hlavní soubor.

  • Vytvoříme DI container
  • Nastavíme umístění konfigurace (složka /config)
  • Spustíme router

To je celé kouzlo.

<?php

use Gephart\DependencyInjection\Container;
use Gephart\Configuration\Configuration;
use Gephart\Routing\Router;

include_once __DIR__ . "/vendor/autoload.php";

$container = new Container();

$configuration = $container->get(Configuration::class);
$configuration->setDirectory(__DIR__ . "/config");

$router = $container->get(Router::class);
$router->run();

/config/routing.json

Nastavení automatického natažení rout z anotací controllerů.

{
  "autoload": "src/"
}

/src/App/Controller/DefaultController.php

<?php

namespace App\Controller;

use Gephart\Response\Response;

class DefaultController
{
    /**
     * @Route /
     */
    public function index() {
        return new Response("Hello World");
    }
}

Celá magie spočívá v anotaci. Ta routeru řekne, že má zavolat danou metodu na dotaz: http://www.example.cz/.

Lze s tím samozřejmě dělat i lepší kousky, například předat z dotazu parametry přímo metodě a říct jim, že musejí odpovídat regulárnímu výrazu.


/**
 * @Route {
 *  "rule": "/page/{slug}/{limit}/{offset}",
 *  "name": "page_detail",
 *  "requirements": {
 *      "limit": "[0-9]+",
 *      "offset": "[0-9]+"
 *  }
 * }
 */
public function index($slug, $limit, $offset) {
    return new Response("Hello " . $slug);
}

RoutePrefix

Kromě anotace @Route je připravená ještě @RoutePrefix, která se dá nastavit přímo třídě a veškeré routy v ním nemusejí dokola psát například „/admin/…“.

Nasledující příklad například odpovídá dotazu „/admin/page“.

<?php

namespace App\Controller;

use Gephart\Response\Response;

/**
 * @RoutePrefix /admin
 */
class AdminController
{
    /**
     * @Route /page
     */
    public function index() {
        return new Response("Hello Admin");
    }
}

Generování URL

Samozřejmě je ještě nutné naopak generovat URL podle názvu routy. V takovém případě je potřeba natáhnout závislost na router v konstruktoru a v případě potřeby zavolat metodu generateUrl().

<?php

namespace App\Controller;

use Gephart\Response\Response;
use Gephart\Routing\Router;

class DefaultController
{

    /**
     * @var Router
     */
    private $router;

    public function __construct(Router $router)
    {
        $this->router = $router;
    }

    /**
     * @Route {
     *  "rule": "/page/{slug}/{limit}/{offset}",
     *  "name": "page_detail",
     *  "requirements": {
     *      "limit": "[0-9]+",
     *      "offset": "[0-9]+"
     *  }
     * }
     */
    public function index($slug, $limit, $offset) {
        $url = $this->router->generateUrl("page_detail", [
            "slug" => "stranka",
            "limit" => "10",
            "offset" => "20",
        ]);

        return new Response("Hello World - " . $url);
    }
}

Třešnička na závěr

Jeden takový malý bonus. Výsledný výstup z Response() lze zachytit pomocí události Router::RESPONSE_RENDER_EVENT, které nese jako parametr celý výsledný řetězec. Stačí zaregistrovat listener v hlavním souboru.

index.php

<?php

use Gephart\DependencyInjection\Container;
use Gephart\Configuration\Configuration;
use Gephart\Routing\Router;

include_once __DIR__ . "/vendor/autoload.php";

$container = new Container();

$configuration = $container->get(Configuration::class);
$configuration->setDirectory(__DIR__ . "/config");

$container->get(\App\EventListener\ResponseListener::class);

$router = $container->get(Router::class);
$router->run();

src/App/EventListener/ResponseListener.php

<?php

namespace App\EventListener;

use Gephart\EventManager\Event;
use Gephart\EventManager\EventManager;
use Gephart\Routing\Router;

class ResponseListener
{
    public function __construct(EventManager $event_manager)
    {
        $event_manager->attach(Router::RESPONSE_RENDER_EVENT, [$this, "reponseRender"]);
    }

    public function responseRender(Event $event)
    {
        $response = $event->getParam("response");
        $response .= "Hello by listener";

        $event->setParams([
            "response" => $response
        ]);
    }
}

Kód včetně testu je opět na GitHubu: https://github.com/gephart/routing.

Instalovat lze jednoduše přes composer:

composer require gephart/routing

Komentáře