tobento / service-routing
A flexible PHP router.
Requires
- php: >=8.4
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- tobento/service-autowire: ^2.0
- tobento/service-collection: ^2.0
- tobento/service-dater: ^2.0
- tobento/service-middleware: ^2.0
- tobento/service-support: ^2.0
- tobento/service-uri: ^2.0
Requires (Dev)
- laminas/laminas-httphandlerrunner: ^2.12
- nyholm/psr7: ^1.8
- nyholm/psr7-server: ^1.1
- phpunit/phpunit: ^12.3
- tobento/service-container: ^2.0
- vimeo/psalm: ^6.13
README
The Routing Service provides a flexible way to build routes for any PHP application.
Table of Contents
- Getting started
- Documentation
- Credits
Getting started
Add the latest version of the routing service running this command.
composer require tobento/service-routing
Requirements
- PHP 8.4 or greater
Highlights
- Basic routing (GET, POST, PUT, PATCH, UPDATE, DELETE)
- Domain routing
- Group routing
- Resource routing
- Named routes
- Matched route handling
- Url and signed url generation
- PSR-15 middleware support
- Localization
- Autowiring
- Framework-agnostic, will work with any project
- Decoupled design
- Easily extendable or customizable
Documentation
Router
use Tobento\Service\Routing\Router; use Tobento\Service\Routing\RequestData; use Tobento\Service\Routing\UrlGenerator; use Tobento\Service\Routing\RouteFactory; use Tobento\Service\Routing\RouteDispatcher; use Tobento\Service\Routing\Constrainer\Constrainer; use Tobento\Service\Routing\RouteHandler; use Tobento\Service\Routing\MatchedRouteHandler; use Tobento\Service\Routing\RouteResponseParser; // Any PSR-11 container $container = new \Tobento\Service\Container\Container(); $router = new Router( new RequestData( $_SERVER['REQUEST_METHOD'] ?? 'GET', rawurldecode($_SERVER['REQUEST_URI'] ?? ''), 'example.com', ), new UrlGenerator( 'https://example.com/basepath', 'a-random-32-character-secret-signature-key', ), new RouteFactory(), new RouteDispatcher($container, new Constrainer()), new RouteHandler($container), new MatchedRouteHandler($container), new RouteResponseParser(), ); $router->setBaseUri('/path/app/');
Basic Routing
Routing methods
$router->get('blog', [Controller::class, 'method']); $router->post('blog', [Controller::class, 'method']); $router->put('blog', [Controller::class, 'method']); $router->patch('blog', [Controller::class, 'method']); $router->delete('blog', [Controller::class, 'method']); $router->head('blog', [Controller::class, 'method']); $router->options('blog', [Controller::class, 'method']); // Route multiple $router->route('GET|POST', 'blog', [Controller::class, 'method']); // Route any $router->route('*', 'blog', [Controller::class, 'method']);
Uri definitions
$router->get('blog/{slug}', 'Controller::method'); // you can define as many as you want: $router->get('blog/{slug}/comment/{id}', 'Controller::method'); // using a question mark for optional parameters $router->get('{?locale}/blog/{?id}', 'Controller::method'); // or using wildcard to allow any path $router->get('blog/{path*}', 'Controller::method');
Handler definitions
The default RouteHandler supports autowiring and the following handler definitions.
// By providing class and method name: $router->get('blog', [Controller::class, 'method']); $router->get('blog', [new Controller(), 'method']); $router->get('blog', Controller::class); // __invoke method called // Using Class::method syntax: $router->get('blog', 'Controller::method'); // Using closure: $router->get('blog', function() { return 'welcome'; }); // You might provide data for build-in method parameters: $router->get('blog', [Controller::class, 'method', ['name' => 'value']]);
Parameters
Name a route:
The main purpose for named routes is the generation of URLs. But they might be useful for any other cases too.
$router->get('blog', 'Controller::method')->name('blog');
⚠️ Named routes should be unique, otherwise the route got overwritten.
Adding middleware: see also With PSR-15 Middleware
$router->get('blog', 'Controller::method') ->middleware(Middleware::class, Another::Middleware);
Where constraint parameter: see also Constrainer
$router->get('blog/{slug}', 'Controller::method') ->where('slug', '[a-z]+'); $router->get('{path*}', 'Controller::method') ->where('path', '[^?]+');
Query constraint parameter:
// do not allow any uri query parameters: $router->get('blog/{slug}', 'Controller::method')->query(null); // allow only certain query characters: $router->get('blog/{slug}', 'Controller::method')->query('/^[a-zA-Z0-9=&\/,\[\]-]+?$/');
Domain: see also Domain Routing
$router->get('blog', [Controller::class, 'method']) ->domain('sub.example.com');
Signed: see also Signed Routing
$router->get('blog', [Controller::class, 'method']) ->signed('blog', validate: false); // default validate: true
Matches:
use Tobento\Service\Routing\RouteInterface; $router->get('{slug}', 'BlogController::method') ->matches(function(SlugsRepo $slugs, RouteInterface $route): null|RouteInterface { // we would need call matches handler later on RouteDispatcher in order to have request data $slug = $slugs->find($route->getParameter('request_parameters')['slug']); if (!$slug || $slug->getResourceKey() !== 'blog') { return null; } $requestParams = $route->getParameter('request_parameters', []); $requestParams['id'] = $slug->getResourceId(); unset($requestParams['slug']); $route->parameter('request_parameters', $requestParams); return $route; }); $router->get('{slug}', 'ProductsController::method') ->matches(function(SlugsRepo $slugs, RouteInterface $route): null|RouteInterface { $slug = $slugs->find($route->getParameter('request_parameters')['slug']); if (!$slug || $slug->getResourceKey() !== 'products') { return null; } $requestParams = $route->getParameter('request_parameters', []); $requestParams['id'] = $slug->getResourceId(); unset($requestParams['slug']); $route->parameter('request_parameters', $requestParams); return $route; });
BaseUrl:
$router->get('blog', [Controller::class, 'method']) ->baseUrl('https:://sub.example.com/app');
Adding custom parameters:
$router->get('blog', [Controller::class, 'method']) ->parameter('name', 'value');
Url Generation
The router provides a url() method for generating URLs from named routes:
use Tobento\Service\Routing\UrlInterface; $router->get('blog', [Controller::class, 'method']) ->name('blog'); var_dump($router->url('blog') instanceof UrlInterface); // bool(true) $blogUrl = $router->url('blog')->get(); $blogUrl = (string) $router->url('blog'); // if your route uri has parameters: $router->get('blog/edit/{id}', [Controller::class, 'method']) ->name('blog.edit'); $blogUrl = $router->url('blog.edit', ['id' => 5])->get();
Throwing vs. Non-Throwing Behavior
By default, the router throws a UrlException if the route name does not exist:
use Tobento\Service\Routing\UrlException; $router->url('missing.route'); // throws UrlException
Safe Mode (throw: false)
You may disable exceptions by passing throw: false:
use Tobento\Service\Routing\NullUrl; $url = $router->url(name: 'missing.route', parameters: [], throw: false);
In this case, the router returns an instance of NullUrl, which also implements UrlInterface.
A NullUrl:
- can be safely cast to a string (resulting in an empty string)
- provides the attempted route name via
name() - provides the attempted parameters via
parameters() - supports all
UrlInterfacemethods with safe defaults
This is useful when generating links in views, notifications, or emails where the route may not exist in all application contexts.
More Routes methods
Custom Routes:
use Tobento\Service\Routing\RouteInterface; // must implement RouteInterface $router->addRoute(new CustomRoute()); $router->addRoutes([ new CustomRoute(), new CustomRoute(), ]);
Get All Routes:
use Tobento\Service\Routing\RouteInterface; $routes = $router->getRoutes(); // returns: array<int|string, RouteInterface>
Get Named Route:
use Tobento\Service\Routing\RouteInterface; $route = $router->getRoute('name'); // returns: null|RouteInterface
Get Matched Route:
use Tobento\Service\Routing\RouteInterface; $matchedRoute = $router->getMatchedRoute(); // returns: null|RouteInterface
Group Routing
You might use groups to share parameters across routes:
use Tobento\Service\Routing\RouteGroupInterface; $router->group('admin', function(RouteGroupInterface $group) { // supports any basic routing methods: $group->get('blog', [Controller::class, 'method'])->name('admin.blog'); // resources: $group->resource('products', ProductsController::class); // The group uri 'admin' gets prepended to route names for resources only. // $router->getRoute('admin.products.index'); // $router->url('admin.products.index'); // group: $group->group('account', function(RouteGroupInterface $group) { // define routes. }); // you might overwrite the group parameters by defining it: $group->get('blog', [Controller::class, 'method']) ->middleware(Middleware::class); })->domain('sub.example.com') ->middleware(Middleware::class) ->baseUrl('sub.example.com') ->parameter('name', 'value') ->locale('de') ->locales(['de', 'en']) ->localeOmit('de') ->localeName('locale') ->localeFallbacks(['de' => 'en']);
If the group uri definition has parameters, they are available on the routes:
use Tobento\Service\Routing\RouteGroupInterface; $router->group('{locale}', function(RouteGroupInterface $group) { // locale is available too. $group->get('blog/{id}', function($locale, $id) { // do something }); })->where('locale', ':in:de:fr');
Resource Routing
You may use resource routing for convenience:
$router->resource('products', ProductsController::class);
This will produce the following routes:
| Method | Uri | Action / Controller method | Route name |
|---|---|---|---|
| GET | products | index | products.index |
| GET | products/create | create | products.create |
| POST | products | store | products.store |
| GET | products/{id} | show | products.show |
| GET | products/{id}/edit | edit | products.edit |
| PUT/PATCH | products/{id} | update | products.update |
| DELETE | products/{id} | delete | products.delete |
You might route only specific actions:
$router->resource('products', ProductsController::class) ->only(['index', 'show']); $router->resource('products', ProductsController::class) ->except(['delete']);
You might change the default where constraint:
$router->resource('products', ProductsController::class) ->where('[a-z0-9]+'); // default is [0-9]+
Adding new or overwriting existing actions:
// creating new action: $router->resource('products', ProductsController::class) ->action( action: 'display', method: 'GET', uri: '/display/{id}', parameters: ['constraints' => ['id' => '[0-9]+']], ); // GET, products/display/{id}, display, products.display // overwriting index action: $router->resource('products', ProductsController::class) ->action( action: 'index', method: 'GET', uri: '/index', parameters: [], ); // GET, products/index, index, products.index
Middleware:
$router->resource('products', ProductsController::class) ->middleware( ['show'], // empty array for all actions Middleware::class, AnotherMiddleware::class, );
Adding additional route parameters for an action:
$router->resource('products', ProductsController::class) ->parameter( action: 'index', name: 'foo', value: 'bar', );
Adding additional route parameters for all actions:
$router->resource('products', ProductsController::class) ->sharedParameter( name: 'foo', value: 'bar', );
With localization and translation:
$router->resource('{?locale}/{products}', ProductsResource::class) // specify the name as above we set the uri: ->name('products') // specify the locales: ->locales(['de', 'en']) ->localeOmit('en') ->localeFallbacks(['de' => 'en']) // specify the translations for the verbs: ->trans('create', ['de' => 'neu', 'en' => 'create'], action: 'create') ->trans('edit', ['de' => 'bearbeiten', 'en' => 'edit'], action: 'edit') // for products: ->trans('products', ['de' => 'produkte', 'en' => 'products']); // Example with new action: $router->resource('products', ProductsController::class) ->action( action: 'display', method: 'GET', uri: '/display/{id}', // set without {display} parameters: ['constraints' => ['id' => '[0-9]+']], ) // specify the locales: ->locales(['de', 'en']) ->localeOmit('en') ->localeFallbacks(['de' => 'en']) // specify the translations for the display verb: ->trans('display', ['de' => 'ansicht', 'en' => 'display'], action: 'display');
Domain Routing
Domain Routes
// route: $router->get('blog', [Controller::class, 'method']) ->domain('example.com'); // group: $router->group('api', function($group) {}) ->domain('api.example.com'); // resource: $router->resource('products', ProductsController::class) ->domain('example.com');
multiple domains
You may add a route for multiple domains:
$router->get('blog', [Controller::class, 'method']) ->domain('example.ch') ->domain('example.de');
domain specific parameters
You may set domain specific parameters for each domain:
use Tobento\Service\Routing\RouteInterface; $router->get('{?locale}/{about}', function($locale, $about) { return [$locale, $about]; }) ->name('about') // default parameters will be used if not specified on domain: ->trans('about', ['de' => 'ueber-uns', 'en' => 'about', 'fr' => 'se-presente']) ->domain('example.ch', function(RouteInterface $route): void { $route->locales(['de', 'fr']) ->localeOmit('de') ->localeFallbacks(['fr' => 'de']) ->trans('about', ['de' => 'ueber-uns', 'fr' => 'se-presente']); })->domain('example.de', function(RouteInterface $route): void { $route->locales(['de', 'en']) ->localeOmit('de') ->localeFallbacks(['en' => 'de']); });
Domain Url Generation
$router->get('blog', [Controller::class, 'method']) ->name('blog') ->domain('example.ch') ->domain('example.de'); // current domain url: $url = $router->url('blog'); // get specific domain url: $url = $router->url('blog')->domain('example.de'); // get all domained urls: $urls = $router->url('blog')->domained(); /*[ 'example.ch' => 'https://example.ch/blog', 'example.de' => 'https://example.de/blog', ]*/ // you may get all translated urls from current or specific domain: $urls = $router->url('blog')->translated(); $urls = $router->url('blog')->domain('example.de')->translated();
Managing Domains
You may specify the domains in order to managing them at one place.
use Tobento\Service\Routing\Router; use Tobento\Service\Routing\RequestData; use Tobento\Service\Routing\UrlGenerator; use Tobento\Service\Routing\RouteFactory; use Tobento\Service\Routing\RouteDispatcher; use Tobento\Service\Routing\Constrainer\Constrainer; use Tobento\Service\Routing\RouteHandler; use Tobento\Service\Routing\MatchedRouteHandler; use Tobento\Service\Routing\RouteResponseParser; use Tobento\Service\Routing\Domains; use Tobento\Service\Routing\Domain; // Any PSR-11 container $container = new \Tobento\Service\Container\Container(); // Domains $domains = new Domains( new Domain(key: 'example.ch', domain: 'ch.localhost', uri: 'http://ch.localhost'), new Domain(key: 'example.de', domain: 'de.localhost', uri: 'http://de.localhost'), ); $router = new Router( new RequestData( $_SERVER['REQUEST_METHOD'] ?? 'GET', rawurldecode($_SERVER['REQUEST_URI'] ?? ''), 'ch.localhost', ), new UrlGenerator( 'https://example.com/basepath', 'a-random-32-character-secret-signature-key', ), new RouteFactory($domains), // pass domains new RouteDispatcher($container, new Constrainer()), new RouteHandler($container), new MatchedRouteHandler($container), new RouteResponseParser(), ); // Adding Routes: // Set the domain name as the key specified above: $router->get('blog', [Controller::class, 'method']) ->domain('example.ch') ->domain('example.de');
Signed Routing
Signed Routes
Add a signed route:
use Tobento\Service\Routing\InvalidSignatureException; $router->get('unsubscribe/{user}', [Controller::class, 'method']) ->signed('unsubscribe'); try { $matchedRoute = $router->dispatch(); } catch (InvalidSignatureException $e) { // handle invalid signature }
Add a signed route with validating on handler for custom response:
use Tobento\Service\Routing\RouterInterface; $router->get('unsubscribe/{user}', function(RouterInterface $router, $user) { $matchedRoute = $router->getMatchedRoute(); $requestUri = $router->getRequestData()->uri(); if (! $router->getUrlGenerator()->hasValidSignature($matchedRoute->getUri(), $requestUri)) { // handle invalid signature. } }) ->signed('unsubscribe', validate: false);
Signed Url Generation
use Tobento\Service\Dater\Dater; $router->get('unsubscribe/{user}', [Controller::class, 'method']) ->signed('unsubscribe'); // generate a signed url with no expiring. $url = (string) $router->url('unsubscribe', ['user' => 5])->sign(); // https://example.com/basepath/unsubscribe/5/a0df83344703b26cd1f9cdcb05196082a6a7799e84b4748a5610d3256b556c55 // generate a signed url which expires in 10 days. $url = (string) $router->url('unsubscribe', ['user' => 5])->sign(new Dater()->addDays(10)); // https://example.com/basepath/unsubscribe/5/a0df83344703b26cd1f9cdcb05196082a6a7799e84b4748a5610d3256b556c55/1630752459 // generate a signed url with no expiring and add signature data as query parameters. $url = (string) $router->url('unsubscribe', ['user' => 5])->sign(withQuery: true); // https://example.com/basepath/unsubscribe/5?signature=6d632a4a8981b1fb017ad6f82067370d6c98ddcd8c6d18cb4fc30c1d44e0f67e // generate a signed url which expires in 10 days and add signature data as query parameters. $url = (string) $router->url('unsubscribe', ['user' => 5])->sign(new Dater()->addDays(10), true); // https://example.com/basepath/unsubscribe/5?expires=1630752540&signature=6d632a4a8981b1fb017ad6f82067370d6c98ddcd8c6d18cb4fc30c1d44e0f67e
Localization and Translation Routing
Localize Routes
$router->get('{?locale}/about', [Controller::class, 'method']) ->name('about'); // get current locale url: $url = (string) $router->url('about'); // https://example.com/basepath/about // get specific locale url: $url = (string) $router->url('about')->locale('en'); // https://example.com/basepath/en/about // get all translated urls: $urls = $router->url('about')->translated(); /*[]*/ // get/create specific translated urls: $urls = $router->url('about')->translated(['de', 'fr']); /*[ 'de' => 'https://example.com/basepath/de/about', 'fr' => 'https://example.com/basepath/fr/about', ]*/
Support only specific locales:
$router->get('{?locale}/about', [Controller::class, 'method']) ->name('about') ->locales(['de', 'en']); // the supported locales, MUST be called before any other locale methods. // get current locale url: $url = (string) $router->url('about'); // https://example.com/basepath/about // get specific locale url: $url = (string) $router->url('about')->locale('en'); // https://example.com/basepath/en/about // get all translated urls: $urls = $router->url('about')->translated(); /*[ 'de' => 'https://example.com/basepath/de/about', 'en' => 'https://example.com/basepath/en/about', ]*/ // get/create specific translated urls: $urls = $router->url('about')->translated(['de', 'fr']); /*[ 'de' => 'https://example.com/basepath/about', 'fr' => 'https://example.com/basepath/fr/about', ]*/
Omit locale in request uri:
$router->get('{?locale}/about', [Controller::class, 'method']) ->name('about') ->locales(['de', 'en']) ->localeOmit('en'); // get current locale url: $url = (string) $router->url('about'); // https://example.com/basepath/about // get specific locale url: $url = (string) $router->url('about')->locale('en'); // https://example.com/basepath/about // get all translated urls: $urls = $router->url('about')->translated(); /*[ 'de' => 'https://example.com/basepath/de/about', 'en' => 'https://example.com/basepath/about', ]*/
Define current locale:
$router->get('{locale}/about', [Controller::class, 'method']) ->name('about') ->locale('en'); // get current locale url: $url = (string) $router->url('about'); // https://example.com/basepath/en/about // get specific locale url: $url = (string) $router->url('about')->locale('de'); // https://example.com/basepath/de/about // get all translated urls: $urls = $router->url('about')->translated(); /*[ 'en' => 'https://example.com/basepath/en/about', ]*/
Rename locale uri definition:
$router->get('{?loc}/about', [Controller::class, 'method']) ->name('about') ->localeName('loc'); // the locale uri definition name, 'locale' is the default name
Translatable Routes
Without locale uri definition
$router->get('/{about}', [Controller::class, 'method']) ->name('about') ->locale('de') // set the current locale. ->trans('about', ['de' => 'ueber-uns', 'en' => 'about']); // get current locale url: $url = (string) $router->url('about'); // https://example.com/basepath/ueber-uns // get specific locale url: $url = (string) $router->url('about')->locale('en'); // https://example.com/basepath/about // get all translated urls: $urls = $router->url('about')->translated(); /*[ 'de' => 'https://example.com/basepath/ueber-uns', 'en' => 'https://example.com/basepath/about', ]*/
Support only specific locales:
$router->get('/{about}', [Controller::class, 'method']) ->name('about') ->locales(['en']) // the supported locales, MUST be called before any other locale methods. ->locale('de') // set the current locale. ->trans('about', ['de' => 'ueber-uns', 'en' => 'about']); // get current locale url: $url = (string) $router->url('about'); // https://example.com/basepath/ueber-uns // get specific locale url: $url = (string) $router->url('about')->locale('en'); // https://example.com/basepath/about // get all translated urls: $urls = $router->url('about')->translated(); /*[ 'en' => 'https://example.com/basepath/about', ]*/
Define locale fallbacks:
$router->get('/{about}', [Controller::class, 'method']) ->name('about') ->locales(['en', 'de', 'fr']) ->localeFallbacks(['fr' => 'en']) ->locale('de') // set the current locale. ->trans('about', ['de' => 'ueber-uns', 'en' => 'about']); // get current locale url: $url = (string) $router->url('about'); // https://example.com/basepath/ueber-uns // get specific locale url: $url = (string) $router->url('about')->locale('fr'); // https://example.com/basepath/about // get all translated urls: $urls = $router->url('about')->translated(); /*[ 'en' => 'https://example.com/basepath/about', 'de' => 'https://example.com/basepath/ueber-uns', 'fr' => 'https://example.com/basepath/about', ]*/
With locale uri definition:
$router->get('{?locale}/{about}', [Controller::class, 'method']) ->name('about') ->localeOmit('en') ->trans('about', ['de' => 'ueber-uns', 'en' => 'about']); // get current locale url: $url = (string) $router->url('about'); // https://example.com/basepath/about // get specific locale url: $url = (string) $router->url('about')->locale('de'); // https://example.com/basepath/de/ueber-uns // get all translated urls: $urls = $router->url('about')->translated(); /*[ 'de' => 'https://example.com/basepath/de/ueber-uns', 'en' => 'https://example.com/basepath/about', ]*/
Default parameters are always prioritized:
$router->get('/{about}', [Controller::class, 'method']) ->name('about') ->locale('de') // set the current locale. ->trans('about', ['de' => 'ueber-uns', 'en' => 'about']); $url = (string) $router->url('about', ['locale' => 'de', 'about' => 'ueberuns'])->locale('en'); // https://example.com/basepath/ueberuns
Matched Route Event
The default MatchedRouteHandler supports autowiring.
$router->get('blog/edit', [Controller::class, 'method'])->name('blog.edit'); $router->matched('blog.edit', function() { // do something after the route has been matched. });
Constrainer
Add rule constraint to route:
// instead of: $router->get('blog/{word}', 'Controller::method') ->where('word', '(foo|bar)'); // you can use a rule: $router->get('blog/{word}', 'Controller::method') ->where('word', ':in:foo:bar'); // using rule with array syntax. $router->get('blog/{word}', 'Controller::method') ->where('word', ['in', 'foo', 'bar']);
Available Rules:
| Rule | Regex | Description |
|---|---|---|
| :alpha | [a-zA-Z]+ | |
| :alpha:2 | [a-zA-Z]{2} | n{x} Matches any string that contains a sequence of X n's |
| :alpha:2:5 | [a-zA-Z]{2,5} | n{x,y} Matches any string that contains a sequence of X to Y n's |
| :alpha:2: | [a-zA-Z]{2,} | n{x,} Matches any string that contains a sequence of at least X n's |
| :num | [0-9]+ | |
| :num:2 | [0-9]{2} | n{x} Matches any string that contains a sequence of X n's |
| :num:2:5 | [0-9]{2,5} | n{x,y} Matches any string that contains a sequence of X to Y n's |
| :num:2: | [0-9]{2,} | n{x,} Matches any string that contains a sequence of at least X n's |
| :alphaNum | [a-zA-Z0-9]+ | |
| :alphaNum:2 | [a-zA-Z0-9]{2} | n{x} Matches any string that contains a sequence of X n's |
| :alphaNum:2:5 | [a-zA-Z0-9]{2,5} | n{x,y} Matches any string that contains a sequence of X to Y n's |
| :alphaNum:2: | [a-zA-Z0-9]{2,} | n{x,} Matches any string that contains a sequence of at least X n's |
| 🆔1:5 | 🆔minNumber:maxLength | |
| :id | Uses the default parameters from the rule 🆔1:21 | |
| :in:foo:bar:baz | If the value is is one of foo, bar, baz |
Custom Rules:
// rule with regex: $router->getRouteDispatcher() ->rule('slug') ->regex('[a-z0-9-]+'); // rule with regex closure: $router->getRouteDispatcher() ->rule('slug') ->regex(function(array $parameters): null|string { // build the regex based on the parameters }); // rule with matches: $router->getRouteDispatcher() ->rule('slug') ->matches(function(string $value, array $parameters): bool { // handle }); // or by adding a rule. $router->getRouteDispatcher()->addRule('slug', new SlugRule());
Dispatching Strategies
There are different ways of handling the matched route, depending on your needs.
Simple
No middleware support though.
use Tobento\Service\Routing\RouteNotFoundException; use Tobento\Service\Routing\InvalidSignatureException; use Tobento\Service\Routing\TranslationException; use Tobento\Service\Routing\RouteInterface; try { $matchedRoute = $router->dispatch(); var_dump($matchedRoute instanceof RouteInterface); // bool(true) } catch (RouteNotFoundException $e) { // handle exception } catch (InvalidSignatureException $e) { // handle exception } catch (TranslationException $e) { // handle exception } // call matched route handler for handling registered matched event actions. $router->getMatchedRouteHandler()->handle($matchedRoute); // handle the matched route. $routeResponse = $router->getRouteHandler()->handle($matchedRoute);
With PSR-7 Response
No middleware support though.
use Tobento\Service\Routing\RouteNotFoundException; use Tobento\Service\Routing\InvalidSignatureException; use Tobento\Service\Routing\TranslationException; try { $matchedRoute = $router->dispatch(); } catch (RouteNotFoundException $e) { // handle exception } catch (InvalidSignatureException $e) { // handle exception } catch (TranslationException $e) { // handle exception } // call matched route handler for handling registered matched event actions. $router->getMatchedRouteHandler()->handle($matchedRoute); // handle the matched route. $routeResponse = $router->getRouteHandler()->handle($matchedRoute); // create response. $response = new \Nyholm\Psr7\Factory\Psr17Factory()->createResponse(200); // parse the route response. $response = $router->getRouteResponseParser()->parse($response, $routeResponse); // emitting response. new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter()->emit($response);
With PSR-15 Middleware
You will need to define your MiddlewareDispatcher implementation on the container. You might customize this behaviour by your own RouteHandler though.
use Tobento\Service\Middleware\MiddlewareDispatcherInterface; use Tobento\Service\Middleware\MiddlewareDispatcher; use Tobento\Service\Middleware\AutowiringMiddlewareFactory; use Tobento\Service\Middleware\FallbackHandler; // adjust route parameters passed to the request attributes if needed: $router->setRequestAttributes(['uri', 'name', 'request_uri']); // After PreRouting or Routing Middleware: $request->getAttribute('route.name'); // Middleware Handling: $container->set(MiddlewareDispatcherInterface::class, function($container) { return new MiddlewareDispatcher( new FallbackHandler(new \Nyholm\Psr7\Factory\Psr17Factory()->createResponse(200)), new AutowiringMiddlewareFactory($container) ); }); $middlewareDispatcher = $container->get(MiddlewareDispatcherInterface::class); // add MethodOverride middleware if needed. $middlewareDispatcher->add(\Tobento\Service\Routing\Middleware\MethodOverride::class); // add PreRouting middleware if needed. $middlewareDispatcher->add(\Tobento\Service\Routing\Middleware\PreRouting::class); // ... more middlewares $middlewareDispatcher->add(\Tobento\Service\Routing\Middleware\Routing::class); $request = new \Nyholm\Psr7\Factory\Psr17Factory()->createServerRequest('GET', 'https://example.com'); $response = $middlewareDispatcher->handle($request); // emitting response. new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter()->emit($response);