tereta / route
An HTTP request router implementing the PSR-7 and PSR-15 standards
Requires
- php: >=8.2
- psr/container: ^2.0
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^13.2
- squizlabs/php_codesniffer: ^4.0
Suggests
- tereta/di: To use the router with a dependency injection container for automatic middleware and handler resolution
Provides
None
Conflicts
None
Replaces
None
README
π English | Π ΡΡΡΠΊΠΈΠΉ | Π£ΠΊΡΠ°ΡΠ½ΡΡΠΊΠ°
An HTTP request router implementing the PSR-7 and PSR-15 standards
- https://www.php-fig.org/psr/psr-7/ - PSR-7: HTTP message interfaces
- https://www.php-fig.org/psr/psr-15/ - PSR-15: HTTP Server Request Handlers
The router is built on the interfaces:
- Psr\Http\Server\RequestHandlerInterface -
this interface is the basis of the
Tereta\Route\Routerfacade, theTereta\Route\Chains\Routerchain of responsibility and the handlers (controllers) - Psr\Http\Server\MiddlewareInterface - this interface is the basis of the
Tereta\Route\Middlewarefacade, theTereta\Route\Chains\Middlewarequeue and the middleware itself
Translations: Π ΡΡΡΠΊΠΈΠΉ | Π£ΠΊΡΠ°ΡΠ½ΡΡΠΊΠ°
Table of contents
Requirements
PHP 8.2+ and implementations of PSR-7/PSR-15/PSR-17 (for example, tereta/runtime).
Installation
composer require tereta/route
Quick start
Tereta\Route\Router is the routing facade and the entry point of the application.
An instance has to be created with a context that provides the Response and Stream factories of a Runtime implementation, for example tereta\runtime.
It is compatible with any PSR-7/PSR-15/PSR-17 implementation, for example:
- Laminas\Diactoros
- Laminas\HttpHandlerRunner
- Other PSR-7/PSR-15/PSR-17 implementations, for example Nyholm\Psr7, Slim\Psr7, GuzzleHttp\Psr7
- Or, as recommended, Tereta\Runtime from the Tereta ecosystem.
Example with Tereta\Runtime:
use Tereta\Route\Router;
use Tereta\Route\Contexts\Router as RouterContext;
use Tereta\Route\Routers\Attribute as AttributeRouter;
use Tereta\Runtime\Factories\Response as ResponseFactory;
use Tereta\Runtime\Factories\Stream as StreamFactory;
$router = new Router(new RouterContext(new ResponseFactory(), new StreamFactory()));
$router->addHandler(HelpHandler::class)
->addRoute(AttributeRouter::class);
$response = $router->handle($request);
Example with Laminas\Diactoros and Laminas\HttpHandlerRunner:
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use App\Handlers\Help as HelpHandler;
use Laminas\Diactoros\ResponseFactory;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\StreamFactory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Tereta\Route\Contexts\Router as RouterContext;
use Tereta\Route\Router;
use Tereta\Route\Routers\Attribute as AttributeRouter;
// The PSR-17 factories of Diactoros replace tereta/runtime - the context is typed against interfaces
$context = new RouterContext(new ResponseFactory(), new StreamFactory());
$router = new Router($context);
$router->addHandler(HelpHandler::class)
->addRoute(AttributeRouter::class);
// A PSR-7 request built from globals (Diactoros parses headers, files and body on its own)
$response = $router->handle(ServerRequestFactory::fromGlobals());
(new SapiEmitter())->emit($response);
Handlers (Controllers)
A handler extends Tereta\Route\Abstracts\Handler and receives the context with the factories and the containers:
You may know the MVC pattern - in PSR-7/PSR-15 the component acting as the controller is called a handler, it takes a request and returns a response.
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Tereta\Route\Abstracts\Handler;
use Tereta\Route\Attributes\Router as RouterAttribute;
#[RouterAttribute(path: '/help')]
class HelpHandler extends Handler
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$stream = $this->context->streamFactory->createStream('Help');
return $this->context->responseFactory->createResponse(200)->withBody($stream);
}
}
A handler is declared with the addHandler method of the Tereta\Route\Router class
The attributes of the handler are checked right away: a broken expression, a method in lower case or a middleware class that does not implement Psr\Http\Server\MiddlewareInterface throw an exception at registration, not at the first request.
If no router matched, the chain ends with the default Tereta\Route\Routers\NotFound handler - a 404 response, or 405 if the path matched but the method did not.
Routes
The Tereta\Route\Attributes\Router attribute is declared on the handler class and describes which requests the handler accepts.
It can check scheme, userInfo, host, port, path, query and fragment - the parts of the URI according to RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986), and expression - a regular expression for the path.
Only the parts you set are checked, the empty ones match any value.
The attribute is a read-only object: its values are set once and never change.
#[RouterAttribute(path: '/help')]
#[RouterAttribute(path: '/help/{identifier}', expression: '#^/help/(?<identifier>[0-9a-z]+)$#Usi')]
#[RouterAttribute(scheme: 'https', host: 'tereta.dev', path: '/')]
#[RouterAttribute(path: '/user', methods: [Method::Post])]
class YourHandler extends Handler { /* ... */ }
expression is a regular expression for the path; if it is set, path is only descriptive.
Declare the port only when it is a non-standard one: according to PSR-7 the implementations normalize the default port of the scheme (80 for http, 443 for https) to null, so an explicit port: 443 never matches a request.
methods is a list of HTTP methods the route accepts. Methods are case-sensitive according to RFC 9110, so write them in upper case: 'GET', not 'get'. If it is empty, any method is accepted.
The standard methods are available in the Tereta\Route\Enums\Method enum: methods: [Method::Get, Method::Post] - a typo is caught by the IDE and PHPStan. A string is still accepted for a non-standard method, for example 'PROPFIND'.
A route with GET also accepts HEAD.
If the path matches but the method does not, the response is 405 Method Not Allowed with the Allow header listing the accepted methods.
An OPTIONS request to such a path gets 204 No Content with the same Allow header, unless a route declares OPTIONS itself. The middleware of the handlers is not executed for this response, so a CORS middleware has to be placed before the router.
The named groups of expression become the attributes of the request, so the handler reads them with getAttribute():
#[RouterAttribute(expression: '#^/user/(?<id>\d+)$#', methods: ['GET'])]
class UserHandler extends Handler
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$id = $request->getAttribute('id'); // "42" for /user/42
/* ... */
}
}
All parameters of the route are also available as an array under Tereta\Route\Interfaces\Router::PARAMETERS:
$parameters = $request->getAttribute(RouterInterface::PARAMETERS); // ['id' => '42']
A parameter never replaces an attribute that the request already has. If a middleware before the router has set user, a route with (?<user>...) keeps that value, and the value from the URL is only in PARAMETERS. So the data from the URL can not take the place of, for example, the authenticated user.
Middleware
The Tereta\Route\Attributes\Middleware attribute is declared on the handler class and takes class - the middleware class executed before the route handler:
use Tereta\Route\Abstracts\Handler;
use Tereta\Route\Attributes\Middleware as MiddlewareAttribute;
use Tereta\Route\Attributes\Router as RouterAttribute;
#[RouterAttribute(path: '/help')]
#[MiddlewareAttribute(class: SiteMiddleware::class)]
#[MiddlewareAttribute(class: AuthMiddleware::class)]
class HelpHandler extends Handler { /* ... */ }
Middleware works according to PSR-15: it receives the request and the next handler, and either calls $handler->handle($request) to go further or returns a response of its own.
Any class that implements Psr\Http\Server\MiddlewareInterface can be used, including middleware from other libraries.
If your middleware needs the context of the route handler, extend Tereta\Route\Abstracts\Middleware - then the context is available as $this->context.
See https://www.php-fig.org/psr/psr-15/.
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Tereta\Route\Abstracts\Middleware;
class SiteMiddleware extends Middleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$site = $this->findSite($request->getUri()->getHost());
$this->context->container->set('site', $site);
return $handler->handle($request);
}
}
The queue is assembled by Tereta\Route\Factories\Middleware from the attributes of the handler and runs in the order of declaration: the one declared first is executed first.
Tereta\Route\Middleware\Pipeline passes the request through the queue and at the end calls handle() on the route handler.
A class that does not implement Psr\Http\Server\MiddlewareInterface is rejected with the Tereta\Route\Exceptions\Middleware exception.
Middleware is declared with the handler attribute only - the Tereta\Route\Middleware facade and its chain are assembled by the Tereta\Route\Factories\Middleware factory at the moment the route is handled.
Containers
The containers implement Psr\Container\ContainerInterface according to PSR-11 (Container interface).
See https://www.php-fig.org/psr/psr-11/
The package provides two containers:
Tereta\Route\Registries\Handler- the registry of the registered handlers, filled by theaddHandler()method of theTereta\Route\Routerfacade; theTereta\Route\Routers\Attributerouter looks up the matching route in it.Tereta\Route\Containers\Context- a container of arbitrary values, implementsTereta\Route\Interfaces\Container, which extendsPsr\Container\ContainerInterfacewith theset()andall()methods.
Tereta\Route\Containers\Context exists in two scopes:
- the application container - created together with the
Tereta\Route\Contexts\Routercontext, available as$router->context->containerand as$this->context->applicationinside a handler; - the route container - created by the
Tereta\Route\Factories\Handlerfactory when the handler is instantiated, available as$this->context->container; middleware and routers put the data for a particular handler into it.
Important: Tereta\Route\Containers\Context is a global storage, so if you use swoole, keep in mind that the application container is created once and lives for the whole lifetime of the server.
// application scope - services shared by all the routes
$router->context->container->set('orm', $orm);
class SiteMiddleware extends Middleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->context->container->set('site', $site); // route scope
return $handler->handle($request);
}
}
#[RouterAttribute(path: '/help')]
class HelpHandler extends Handler
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$orm = $this->context->application->get('orm'); // put by the application
$site = $this->context->container->get('site'); // put by the middleware
/* ... */
}
}
Reading an unregistered identifier throws Tereta\Route\Exceptions\Pool, which implements Psr\Container\NotFoundExceptionInterface according to PSR-11.
Writing the same identifier twice throws Tereta\Route\Exceptions\Duplicate, which implements Psr\Container\ContainerExceptionInterface.
The presence of a value is checked with the has() method.
A dependency injection container of any third-party PSR-11 implementation, for example PHP-DI or Laminas\ServiceManager, is placed into the application container as a value - $router->context->container->set('services', $services).
Custom router
A router implements Tereta\Route\Interfaces\Router, which extends Psr\Http\Server\RequestHandlerInterface according to PSR-15, and extends Tereta\Route\Abstracts\Router.
The Tereta\Route\Chains\Router chain is a Chain of Responsibility (GoF), so a router either responds itself or passes the request to the next link by calling parent::handle().
The built-in Tereta\Route\Routers\Attribute router matches routes by attributes, while a custom router is needed when the routes come from somewhere else - a database, a configuration, a cache.
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Tereta\Route\Abstracts\Router;
use Tereta\Route\Containers\Context as ContextContainer;
class DatabaseRouter extends Router
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$pdo = $this->context->container->get('pdo'); // put into the application container
$route = $this->findRoute($pdo, $request->getUri()->getPath());
if ($route === null) {
return parent::handle($request); // pass the request to the next link of the chain
}
// the route container, available in the handler as $this->context->container
$container = new ContextContainer(['route' => $route]);
$instance = $this->context->handlerFactory->create(HelpHandler::class, $this->context, $container);
return $this->context->middlewareFactory->create($instance)->process($request, $instance);
}
}
Routers are declared with the addRoute() method of the Tereta\Route\Router facade.
It takes only the class name: the chain creates the router itself and passes the chain link and the context to its constructor.
The dependencies of a router, like PDO above, are put into the application container:
$router->context->container->set('pdo', $pdo);
$router->addRoute(AttributeRouter::class)
->addRoute(DatabaseRouter::class);
Every next router wraps the previous one (chain of responsibility), therefore the one added last is checked first, and the chain is terminated by Tereta\Route\Routers\NotFound with a 404 response, or 405 if the path matched but the method did not.
A class that does not implement Tereta\Route\Interfaces\Router is rejected with the Tereta\Route\Exceptions\Router exception.
Through the Tereta\Route\Contexts\Router context a router has access to:
handlerFactory- the factory of theTereta\Route\Abstracts\Handlerhandler with the context and the route container;middlewareFactory- the factory that assembles the middleware queue from the attributes of the created handler;attributeService- reads the attributes of a handler once and keeps them, so the next requests do not read them again;handlers- the registry of the registered handlers,uriService- the matching ofPsr\Http\Message\UriInterfaceagainst a route attribute;responseFactoryandstreamFactory- the PSR-17 factories for a response,container- this container is the application container, services shared by all the routes and handlers can be put into it.
License and author
Tereta Alexander tereta.alexander@gmail.com Web: https://tereta.dev Copyright Β©2008-2026 Tereta Alexander License https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
www.ββββββββββββββββββββββββ βββββββββββββββββ ββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββ
βββ ββββββ ββββββββββββββ βββ ββββββββ
βββ ββββββ ββββββββββββββ βββ ββββββββ
βββ βββββββββββ βββββββββββ βββ βββ βββ
βββ βββββββββββ βββββββββββ βββ βββ βββ
.dev