tobento / service-routing
A flexible PHP router.
Requires
- php: >=8.0
- psr/http-message: ^1.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- tobento/service-autowire: ^1.0
- tobento/service-collection: ^1.0
- tobento/service-dater: ^1.0
- tobento/service-middleware: ^1.0
- tobento/service-support: ^1.0
- tobento/service-uri: ^1.0
Requires (Dev)
- laminas/laminas-httphandlerrunner: ^1.4
- nyholm/psr7: ^1.4
- nyholm/psr7-server: ^1.0
- phpunit/phpunit: ^9.5
- tobento/service-container: ^1.0
- vimeo/psalm: ^4.0
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.0 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
Generating url from named routes:
$router->get('blog', [Controller::class, 'method']) ->name('blog'); $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();
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:
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:
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);