sirix / mezzio-routing-attributes
Attribute-based routing support for Mezzio applications
Package info
github.com/sirix777/mezzio-routing-attributes
pkg:composer/sirix/mezzio-routing-attributes
Requires
- php: ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0
- mezzio/mezzio-router: ^3.15 || ^4.1
- psr/container: ^1.0 || ^2.0
- psr/http-server-middleware: ^1.0
- sirix/mezzio-routing-contracts: ^0.1.0
Requires (Dev)
- bamarni/composer-bin-plugin: ^1.8
- laminas/laminas-cli: ^1.15
- laminas/laminas-diactoros: ^3.8
- laminas/laminas-servicemanager: ^3.23
- phpunit/phpunit: ^11.5
- symfony/console: ^6.4 || ^7.0
README
Attribute-based route registration for Mezzio applications.
Pre-1.0 package: usable in controlled applications, but public API and configuration may still change before
1.0.0.
Installation
composer require sirix/mezzio-routing-attributes
Status
This package provides:
- PHP 8 route attributes (
Route,Get,Post,Put,Patch,Delete,Any) - Class-level and method-level attribute extraction
- Route provider registration via
RouteCollectorInterface - Optional route middleware stacks in attributes (
middleware: [...]) - Optional class discovery from configured directories
- Compiled route cache artifact (
require-based) - CLI commands:
routing-attributes:routes:listrouting-attributes:cache:clear
Recommended production mode:
- use an explicit
classeslist; - enable compiled cache;
- clear/warm cache during deploy;
- restart long-running workers after route/cache changes.
Configuration
Production default (performance-first):
return [ 'routing_attributes' => [ 'classes' => [ App\Handler\PingHandler::class, ], 'duplicate_strategy' => 'throw', // throw|ignore 'handlers' => [ 'mode' => 'psr15', // psr15|callable ], 'override_mezzio_routes_list_command' => false, 'route_list' => [ 'classic_routes_middleware_display' => 'upstream', // upstream|resolved ], 'discovery' => [ 'enabled' => false, 'paths' => [], 'strategy' => 'token', // token|psr4 'psr4' => [ 'mappings' => [], 'fallback_to_token' => true, ], ], 'cache' => [ 'enabled' => true, 'file' => 'data/cache/mezzio-routing-attributes.php', ], ], ];
Supported routing_attributes.cache keys:
enabled(bool)file(non-empty string, required whenenabled=true)
The package registers its own factories through ConfigProvider; application handlers and middleware still need to be available in your container.
Discovery Behavior
- If
discovery.enabled=false, only explicitclassesare used. - If
discovery.enabled=true, classes are discovered fromdiscovery.paths. - If compiled cache is enabled and cache file already exists, discovery is skipped on boot.
- Prefer discovery for development or cache warmup, not as the main production boot path.
Compiled Cache Behavior
- If
cache.enabled=trueand cache file exists, routes are registered from compiled cache. - If cache file is missing or invalid, routes are extracted/discovered and cache file is rebuilt.
- Cache format is optimized for startup speed and keeps middleware pipeline resolution lazy per service.
- Ensure the cache directory is writable by the process that warms/rebuilds routes.
Cache Clear Command
Clear compiled cache file:
php vendor/bin/laminas routing-attributes:cache:clear
Override file path:
php vendor/bin/laminas routing-attributes:cache:clear --file=data/cache/custom-routes.php
In RoadRunner/Swoole-style runtimes, reload workers after clearing or rebuilding the cache.
Basic Usage
Method-level attribute:
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Sirix\Mezzio\Routing\Attributes\Attribute\Get; final class PingHandler implements RequestHandlerInterface { #[Get('/ping', name: 'ping')] public function handle(ServerRequestInterface $request): ResponseInterface { throw new \RuntimeException('Implement your response.'); } }
Class-level attribute:
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Sirix\Mezzio\Routing\Attributes\Attribute\Get; #[Get('/ping', name: 'ping')] final class PingHandler implements RequestHandlerInterface { public function handle(ServerRequestInterface $request): ResponseInterface { throw new \RuntimeException('Implement your response.'); } }
Custom Attribute Modifiers
You can create route-related attributes in your own package by implementing
Sirix\Mezzio\Routing\Contracts\RouteAttributeModifierInterface.
Example custom attribute:
namespace Acme\Routing\Attribute; use Attribute; use Sirix\Mezzio\Routing\Contracts\RouteAttributeModifierInterface; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final readonly class RequireTenant implements RouteAttributeModifierInterface { public function __construct(private string $tenantHeader = 'x-tenant-id') {} public function getMiddleware(): array { return [Acme\Middleware\RequireTenantMiddleware::class]; } public function getDefaults(): array { return ['tenant_header' => $this->tenantHeader]; } }
Usage with route attributes:
use Acme\Routing\Attribute\RequireTenant; use Sirix\Mezzio\Routing\Attributes\Attribute\Get; #[RequireTenant('x-tenant-id')] final class OrdersHandler { #[Get('/orders', name: 'orders.list')] #[RequireTenant('x-org-id')] public function index(mixed ...$args): mixed { // ... } }
Route Defaults and Placeholders
The getDefaults() method allows you to provide default values for route placeholders. This is useful when you have optional parameters in your route paths.
Example with optional parameter:
use Attribute; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Sirix\Mezzio\Routing\Attributes\Attribute\Get; use Sirix\Mezzio\Routing\Contracts\RouteAttributeModifierInterface; #[Attribute] final readonly class DefaultFormat implements RouteAttributeModifierInterface { public function __construct(private string $format = 'html') {} public function getMiddleware(): array { return []; } public function getDefaults(): array { return ['format' => $this->format]; } } final class ExportHandler { #[Get('/export/:format?')] #[DefaultFormat('json')] public function __invoke(ServerRequestInterface $request): ResponseInterface { // $request->getAttribute('format') will be 'json' if not provided in URL } }
Notes:
- class-level and method-level modifiers are merged for method routes;
- method-level defaults override class-level defaults on the same key;
- middleware from modifiers is appended after middleware declared in
Route/Getattributes. - defaults are passed to the Mezzio
Route::setOptions()and can be used by the underlying router (like FastRoute) to fill missing optional placeholders.
Benchmarks
Run:
composer benchmark composer benchmark-threshold
Latest local run (PHP 8.2.30):
warm_cache_hit_manual:0.0022 msmedian,2.0156 KBmedian peakno_cache_manual:0.0071 msmedian,3.4453 KBmedian peakcold_cache_rebuild_manual:0.0489 msmedian,5.9063 KBmedian peakwarm_cache_hit_discovery_token:0.0053 msmedian,3.1406 KBmedian peakwarm_cache_hit_discovery_psr4:0.0040 msmedian,3.1406 KBmedian peak- Threshold benchmark (
compiled) showed cache-win from10routes onward. - At
12800routes:22.8902 ms(no-cache) vs8.7022 ms(compiled), speedup61.98%; peak memory13213.21 KBvs9260.80 KB.
These are microbenchmarks for route registration/cache paths, not end-to-end HTTP latency.
Troubleshooting
- Service not found: register handler/action class in container.
- Route changes are not visible: clear compiled cache with
routing-attributes:cache:clear. - In long-running workers (RoadRunner/Swoole), reload/restart workers after cache rebuild/clear.
- Invalid cache payload errors: delete cache file and warm it again.