Zum Inhalt

Routen

Seit Version 5.2.0 nutzt JTL-Shop die PHP-Bibliothek league/route für das Routing, also die Auflösung von URLs. Auch in Plugins können seitdem eigene Routen definiert werden.

Hint

Das allgemeine Demo-Plugin zeigt auch, wie eigene Routen verwendet werden können. Es befindet sich im öffentlichen Gitlab-Repository. Ein komplexeres Beispiel stellt das ebenfalls frei verfügbare Plugin 10 minute rest api dar.

Registrierung

Der Einstiegspunkt zur Registrierung von Routen findet über den Hook HOOK_ROUTER_PRE_DISPATCH statt. Dieser übergibt in seinen Parametern eine Instanz der Klasse \JTL\Router\Router, welche die Methode addRoute(string $slug, callable $cb, ?string $name = null, array $methods = [GET], ?\Psr\Http\Server\MiddlewareInterface $middleware = null bereitstellt.

Es wird dabei stets erwartet, dass der Callback eine Instanz von \Psr\Http\Message\ResponseInterface zurückgibt. Hierfür kann die im Shop mit ausgelieferte Bibliothek laminas-diactoros verwendet werden, die beispielsweise Implementationen normaler Text- oder JSON-Responses bereitstellt.

Ein einfaches Beispiel könnte so aussehen:

<?php declare(strict_types=1);

namespace Plugin\example_routing;

use JTL\Events\Dispatcher;
use JTL\Plugin\Bootstrapper;
use JTL\Router\Router;
use Laminas\Diactoros\Response\JsonResponse;

class Bootstrap extends Bootstrapper
{
    public function boot(Dispatcher $dispatcher): void
    {
        parent::boot($dispatcher);
        $dispatcher->hookInto(\HOOK_ROUTER_PRE_DISPATCH, function (array $args) {
            /** @var Router $router */
            $router = $args['router'];
            $router->addRoute('/jsonexample', function () {
                return new JsonResponse(['foobar' => 42]);
            });
        });
    }
}

Hier wurde die Route <shop-url>/jsonexample erzeugt, die bei Aufruf direkt ein als JSON formatiertes Array zurückliefert.

Smarty-Templates

Wie oben erwähnt, müssen die registrierten Callbacks Instanzen des Interfaces ResponseInterface zurückgeben statt - wie sonst in JTL-Shop üblich - reinen Text wie z.B. gerenderten HTML-Code. Daher sollten an dieser Stelle auf keinen Fall die Smarty-Methoden display() bzw. fetch() genutzt werden. Falls gewünscht ist, den Inhalt eines Smarty-Templates zurückzugeben, wird die Nutzung der Methode \JTL\Smarty\JTLSmarty::getResponse(string $template): ResponseInterface empfohlen. Diese arbeitet analog zu display(), verpackt den gerenderten Text aber in eine Textresponse mit dem HTTP-Statuscode 200.

Hint

Als Parameter erhalten die Callback-Methoden stets 3 Parameter:

  • Psr\Http\Message\ServerRequestInterface $request (das gesendete Request)
  • array $args (die Routen-Parameter)
  • \JTL\Smarty\JTLSmarty $smarty (eine Instanz des Frontend-Renderers)

Der Hook von oben könnte nun so geändert werden:

<?php declare(strict_types=1);

namespace Plugin\example_routing;

use JTL\Events\Dispatcher;
use JTL\Plugin\Bootstrapper;
use JTL\Router\Router;
use JTL\Smarty\JTLSmarty;
use Psr\Http\Message\ServerRequestInterface;

class Bootstrap extends Bootstrapper
{
    public function boot(Dispatcher $dispatcher): void
    {
        parent::boot($dispatcher);
        $dispatcher->hookInto(\HOOK_ROUTER_PRE_DISPATCH, function (array $args) {
            /** @var Router $router */
            $router = $args['router'];
            $router->addRoute('/smartyexample', function (ServerRequestInterface $request, array $args, JTLSmarty $smarty) {
                return $smarty->assign('myVariable', 42)
                    ->getResponse(__DIR__ . '/mytest.tpl');
            });
        });
    }
}

Ruft man nun <shop-url>/smartytest auf, so wird die Templatedatei mytest.tpl aus dem Plugin-Hauptverzeichnis gerendert und dargestellt.

Parameter

Analog zur Dokumentation lassen sich zu den Routen nun noch dynamische URL-Segmente hinzufügen. Ein optionaler Parameter - erkennbar an den eckigen Klammern - könnte so aussehen:

$router->addRoute('/withoptionalparam[/{id}]', function () {});

Diese Route würde sowohl bei Aufruf von <shop-url>/withoptionalparam als auch bei <shop-url>/withoptionalparam/test matchen.

Außerdem können einfache Typen oder komplexe reguläre Ausdrücke vorgegeben werden:

$router->addRoute('/withnumberparam/{id:number}', function () {});

Ein Aufruf der URL <shop-url>/withnumberparam oder <shop-url>/withnumberparam/test würde nun einen 404-Fehler erzeugen, da der Pflichtparameter nicht übergeben wurde bzw. nicht numerisch ist. Die URL <shop-url>/withnumberparam/42 würde hingegen matchen. Der Wert des dynamischen URL-Segements wird immer im zweiten Parameter (hier $args) an die Callbackmethode übergeben.

<?php declare(strict_types=1);

namespace Plugin\example_routing;

use JTL\Events\Dispatcher;
use JTL\Plugin\Bootstrapper;
use JTL\Router\Router;
use JTL\Smarty\JTLSmarty;
use Psr\Http\Message\ServerRequestInterface;

class Bootstrap extends Bootstrapper
{
    public function boot(Dispatcher $dispatcher): void
    {
        parent::boot($dispatcher);
        $dispatcher->hookInto(\HOOK_ROUTER_PRE_DISPATCH, function (array $args) {
            /** @var Router $router */
            $router = $args['router'];
            $router->addRoute('/smarty/{examplenumber:number}', function (ServerRequestInterface $request, array $args, JTLSmarty $smarty) {
                return $smarty->assign('myVariable', $args['examplenumber'])
                    ->getResponse('string:<p>Number:</p>{$myVariable}');
            });
        });
    }
}

Im obigen Beispiel wurde die Pflichtangabe examplenumber als number definiert und im Callback aus dem Parameter $args extrahiert und der Smarty-Variablen $myVariable zugewiesen. Anschließend erfolgt die Ausgabe des gerenderten Templates. Ruft man nun die Seite <shop-url>/smarty/42 auf, so sollte nun die Ausgabe Number: 42 erscheinen.

Hint

Smarty unterstützt auch das Rendering via string: - dies erspart in den Beispielen die Darstellung einer weiteren Datei. Die Ausgabe von $smarty->getRespone('string:Hallo {$test}'); ist analog zu $smarty->getResponse('test.tpl'); wobei die Datei test.tpl den Inhalt Hallo {$test} hat.

Middleware

Via JTL\Router\Router::adRoute()kann optional auch eineMiddleware definiert werden.

Hierfür wird im Plugin-Hauptverzeichnis eine neue Klasse angelegt, die das Interface Psr\Http\Server\MiddlewareInterface implementiert:

<?php declare(strict_types=1);

namespace Plugin\example_routing;

use Laminas\Diactoros\Response\RedirectResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ExampleMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if (random_int(0, 2) === 1) {
            return new RedirectResponse('https://google.de/');
        }
        return $handler->handle($request);
    }
}

Anschließend wird sie registriert:

<?php declare(strict_types=1);

namespace Plugin\example_routing;

use JTL\Events\Dispatcher;
use JTL\Plugin\Bootstrapper;
use JTL\Router\Router;
use JTL\Smarty\JTLSmarty;
use Psr\Http\Message\ServerRequestInterface;

class Bootstrap extends Bootstrapper
{
    public function boot(Dispatcher $dispatcher): void
    {
        parent::boot($dispatcher);
        $dispatcher->hookInto(\HOOK_ROUTER_PRE_DISPATCH, function (array $args) {
            /** @var Router $router */
            $router = $args['router'];
            $router->addRoute(
                '/smarty/{examplenumber:number}',
                function (ServerRequestInterface $request, array $args, JTLSmarty $smarty) {
                    return $smarty->assign('myVariable', $args['examplenumber'])
                        ->getResponse('string:<h1>test</h1>My variable:{$myVariable}');
                },
                'myCustomRouteName',
                ['GET', 'POST'],
                new ExampleMiddleware()
            );
        });
    }
}

Die URL <shop-url>/smarty/42 kann nun via GET oder POST aufgerufen werden und würde statistisch bei einem von 3 Aufrufen zu google.de weiterleiten statt das Template darzustellen.

Exceptions

Die Methode addRoute() wirft eine Exception vom Typ FastRoute\BadRouteException, falls die Route nicht registrierbar sein sollte. Dies ist insbesondere dann der Fall, wenn dieselbe URL mehrfach verwendet wird. Es bietet sich daher an, den Methodenaufruf in einen Try-Catch-Block zu verpacken:

$router->addRoute('/exampleroute', function () {});
try {
    $router->addRoute('/exampleroute', function () {});
} catch (Exception $e) {
  // FastRoute\BadRouteException: Cannot register two routes matching "/exampleroute" for method "GET"
}

URLs generieren

Auch umgekehrt lassen sich mit eigenen Routen aus Parametern heraus die URL-Pfade generieren.

<?php declare(strict_types=1);

namespace Plugin\example_routing;

use JTL\Events\Dispatcher;
use JTL\Plugin\Bootstrapper;
use JTL\Router\Router;
use JTL\Shop;

class Bootstrap extends Bootstrapper
{
    public function boot(Dispatcher $dispatcher): void
    {
        parent::boot($dispatcher);
        $dispatcher->hookInto(\HOOK_ROUTER_PRE_DISPATCH, function (array $args) {
            /** @var Router $router */
            $router = $args['router'];
            $router->addRoute(
                '/year/{year:number}/month/{month:number}/day/{day:word}',
                function() { /** ... */},
                'myDateRoute',
                ['GET', 'POST']
            );
        });
    }

    public function getMyPath(): void
    {
        $router = Shop::getRouter();
        $path   = $router->getNamedPath('myDateRouteGET', ['year' => 1984, 'month' => 9, 'day' => 'Tuesday']);
        dump($path); // /year/1984/month/9/day/Tuesday
    }
}

Im obigen Beispiel wurde eine Route mit 3 dynamischen Teilen generiert - year, month und day. Möchte man nun an einer beliebigen Stelle eine URL erzeugen, lässt sich dies anhand des Routennamens (myDateRoute) und der gewünschten HTTP-Methode (hier GET) über die Methode \JTL\Router\Router::getNamedPath(string $name, ?array $replacements = null): string tun.

Als zweiten Parameter erwartet die Methode ein Array bei dem die Key-Namen jeweils den einzelnen dynamischen URL-Teilen entsprechen.