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
This package is not auto-updated.
Last update: 2026-08-08 13:17:39 UTC
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\Middlewarechain of responsibility 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 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
If no router matched, the chain ends with the default Tereta\Route\Routers\NotFound handler - a 404 response.
Routes
The Tereta\Route\Attributes\Router attribute is built on the Psr\Http\Message\UriInterface interface and declares the route configuration, which may include scheme, host, port, path and expression to identify the parts of the route according to RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986).
If expression is set, path is only descriptive. userInfo, query and fragment described in PSR-7 (HTTP message interfaces) and in the Psr\Http\Message\UriInterface interface are available as well
See https://www.php-fig.org/psr/psr-7/
#[RouterAttribute(path: '/help')]
#[RouterAttribute(path: '/help/{identifier}', expression: '#^/help/(?<identifier>[0-9a-z]+)$#Usi')]
#[RouterAttribute(scheme: 'https', host: 'tereta.dev', path: '/')]
class YourHandler extends Handler { /* ... */ }
expression is a regular expression for the path; if it is set, path is only descriptive. The other properties described in the PSR-7 interface are available as well.
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.
The named groups of the expression take part in the matching only - the Tereta\Route\Services\Uri service returns the result of the match and not the values of the groups, so parse the parts of the path in the handler from $request->getUri()->getPath().
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 { /* ... */ }
The Tereta\Route\Chains\Middleware chain is essentially a Chain of Responsibility (GoF), every link of which passes control to the next one or returns a response of its own.
Your middleware extends Tereta\Route\Abstracts\Middleware and implements Psr\Http\Server\MiddlewareInterface according to PSR-15.
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\Handler;
use Tereta\Route\Abstracts\Middleware;
class SiteMiddleware extends Middleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$site = $this->findSite($request->getUri()->getHost());
/** @var Handler $handler the route handler, a descendant of Tereta\Route\Abstracts\Handler */
$handler->context->container->set('site', $site);
return parent::process($request, $handler);
}
}
The chain is assembled by Tereta\Route\Factories\Middleware from the attributes of the handler: every next declaration wraps the previous one, therefore the one declared last is executed first.
The chain is terminated by Tereta\Route\Middleware\Handler, which 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\Containers\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
{
/** @var Handler $handler */
$handler->context->container->set('site', $site); // route scope
return parent::process($request, $handler);
}
}
#[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
/* ... */
}
}
Writing the same identifier twice, as well as reading an unregistered one, throws Tereta\Route\Exceptions\Pool, which implements Psr\Container\NotFoundExceptionInterface according to PSR-11.
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 PDO;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Tereta\Route\Abstracts\Router;
use Tereta\Route\Containers\Context as ContextContainer;
use Tereta\Route\Contexts\Router as RouterContext;
use Tereta\Route\Interfaces\Router as RouterInterface;
class DatabaseRouter extends Router
{
public function __construct(RouterInterface $link, RouterContext $context, private PDO $pdo)
{
parent::__construct($link, $context);
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$route = $this->findRoute($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, $container);
return $this->context->middlewareFactory->create($instance)->process($request, $instance);
}
}
Routers are declared with the addRoute() method of the Tereta\Route\Router facade; the arguments given after the class name are passed to the router constructor right after the chain link and the context:
$router->addRoute(AttributeRouter::class)
->addRoute(DatabaseRouter::class, $pdo);
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.
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 chain from the attributes of the created handler;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