Search by

tereta / route

tereta

An HTTP request router implementing the PSR-7 and PSR-15 standards

0.0.14 2026-10-10 17:22 UTC

This package is not auto-updated.

Last update: 2026-10-10 14:23:00 UTC


README

🌐 English | Русский | Π£ΠΊΡ€Π°Ρ—Π½ΡΡŒΠΊΠ°

An HTTP request router implementing the PSR-7 and PSR-15 standards

The router is built on the interfaces:

  • Psr\Http\Server\RequestHandlerInterface - this interface is the basis of the Tereta\Route\Router facade, the Tereta\Route\Chains\Router chain of responsibility and the handlers (controllers)
  • Psr\Http\Server\MiddlewareInterface - this interface is the basis of the Tereta\Route\Middleware facade, the Tereta\Route\Chains\Middleware queue 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 the addHandler() method of the Tereta\Route\Router facade; the Tereta\Route\Routers\Attribute router looks up the matching route in it.
  • Tereta\Route\Containers\Context - a container of arbitrary values, implements Tereta\Route\Interfaces\Container, which extends Psr\Container\ContainerInterface with the set() and all() methods.

Tereta\Route\Containers\Context exists in two scopes:

  • the application container - created together with the Tereta\Route\Contexts\Router context, available as $router->context->container and as $this->context->application inside a handler;
  • the route container - created by the Tereta\Route\Factories\Handler factory 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 the Tereta\Route\Abstracts\Handler handler 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 of Psr\Http\Message\UriInterface against a route attribute;
  • responseFactory and streamFactory - 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