Po předchozím komponentách (anotace, dependecy 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