sirix / mezzio-routing-contracts
Contracts for sirix/mezzio-routing-attributes route attribute modifiers
Package info
github.com/sirix777/mezzio-routing-contracts
pkg:composer/sirix/mezzio-routing-contracts
Requires
- php: ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0
- psr/container: ^1.0 || ^2.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- bamarni/composer-bin-plugin: ^1.8
- phpunit/phpunit: ^11.5
- psr/http-message: ^1.0 || ^2.0
- psr/http-server-handler: ^1.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Contracts for sirix/mezzio-routing-attributes route attribute modifiers.
This package contains the stable public contract used by routing attributes that need to contribute middleware or default route options to a route definition.
Installation
composer require sirix/mezzio-routing-contracts
Usage
Implement RouteAttributeModifierInterface on any route-related attribute to inject middleware and/or provide default route options.
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 [RequireTenantMiddleware::class]; } public function getDefaults(): array { return ['tenant_header' => $this->tenantHeader]; } }
Usage with routing attributes:
use Sirix\Mezzio\Routing\Attributes\Attribute\Get; final class OrdersHandler { #[Get('/orders', name: 'orders.list')] #[RequireTenant('x-org-id')] public function list(): ResponseInterface { // ... } }
The routing-attributes package discovers all implementations of RouteAttributeModifierInterface at boot time and merges their middleware and defaults into the route definition.
MiddlewareSpecification
MiddlewareSpecification is a serializable value object that describes how to build a middleware instance via a factory. Use it when an attribute needs to pass recipe-style arguments to a middleware factory instead of returning only a plain service id.
use Attribute; use Sirix\Mezzio\Routing\Contracts\MiddlewareSpecification; use Sirix\Mezzio\Routing\Contracts\RouteAttributeModifierInterface; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final readonly class Authenticated implements RouteAttributeModifierInterface { public function __construct(private string $profile = 'default') {} public function getMiddleware(): array { return [ new MiddlewareSpecification( service: AuthenticatedMiddleware::class, factory: AuthenticatedMiddlewareFactory::class, arguments: ['profile' => $this->profile], ), ]; } public function getDefaults(): array { return []; } }
MiddlewareFactoryInterface
Implement this contract to turn a MiddlewareSpecification into a MiddlewareInterface instance. The factory receives the PSR-11 container and the specification, so it can resolve dependencies and interpret the recipe at pipeline-build time.
use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Sirix\Mezzio\Routing\Contracts\MiddlewareFactoryInterface; use Sirix\Mezzio\Routing\Contracts\MiddlewareSpecification; final readonly class AuthenticatedMiddlewareFactory implements MiddlewareFactoryInterface { public function create( ContainerInterface $container, MiddlewareSpecification $specification, ): MiddlewareInterface { return new AuthenticatedMiddleware(profile: $specification->arguments['profile']); } }
Contract
interface RouteAttributeModifierInterface { /** * @return list<class-string<MiddlewareInterface>|non-empty-string|MiddlewareSpecification> */ public function getMiddleware(): array; /** @return array<string, mixed> */ public function getDefaults(): array; }
AggregatingRouteAttributeModifierInterface
Implement this opt-in extension when repeatable modifiers must accumulate values under
the same defaults key, or when one of their middleware entries must be included only once
per route definition. Existing RouteAttributeModifierInterface implementations keep their
shallow defaults merge and repeatable middleware behavior.
interface AggregatingRouteAttributeModifierInterface extends RouteAttributeModifierInterface { /** * @param array<string, mixed> $defaults * @return array<string, mixed> */ public function mergeDefaults(array $defaults): array; /** * @return array<non-empty-string, MiddlewareSpecification|non-empty-string> */ public function getUniqueMiddleware(): array; }
mergeDefaults()
Receives the defaults accumulated for the route before the modifier is applied and must return the complete defaults array to use afterwards. This lets a modifier append to an existing list instead of replacing it through the ordinary shallow merge.
getUniqueMiddleware()
Returns an associative array whose non-empty string keys are stable middleware identity keys. Prefer
a namespaced key such as vendor.package.middleware. A key identifies exactly one middleware identity
for an entire route definition. Each value is a non-empty middleware service identifier or a
MiddlewareSpecification.
A consumer adds the first value for a key and deduplicates only later values with the same identity.
It must fail closed by rejecting a later value for the same key when its identity differs: string
identifiers are equal only when identical; MiddlewareSpecification values are equal only when their
canonical signature() values are identical; and a string is never equal to a
MiddlewareSpecification. The signature uses a canonical type-tagged encoding of the complete
(service, factory, arguments) tuple, preserving service/factory boundaries, null versus an empty
factory string, scalar and array-key types, exact float representations, key order, and nested arguments.
Unrelated middleware and middleware returned by
getMiddleware() remain repeatable.
getMiddleware()
Returns middleware identifiers that should be appended to the route pipeline. Each item must be one of:
- a middleware class name implementing
Psr\Http\Server\MiddlewareInterface; - another non-empty middleware identifier supported by the consuming router integration;
- a
MiddlewareSpecificationdescribing how to build a middleware instance via a factory.
Existing implementations that return only strings remain valid; the MiddlewareSpecification type is additive.
getDefaults()
Returns default route options keyed by option name. Consumers merge these values into the route defaults/options for the route that carries the attribute.
Caching
Route definitions (including middleware specifications) are exported into PHP route-cache files. Because MiddlewareSpecification is rehydrated through __set_state, its $arguments are restricted to serializable scalars and nested scalar arrays. Route-cache exports contain the plain (service, factory, arguments) properties. Legacy three-property cache entries (1.2.0) and transitional four-property cache entries (1.2.1, with canonicalArguments) remain supported through strict compatibility paths. A transitional entry whose arguments and canonicalArguments disagree is rejected as malformed instead of being silently rehydrated; finite floats are compared by their IEEE-754 bit representation, so 0.0 and -0.0 mismatches are also rejected. All paths reject missing, unknown, or incorrectly typed properties, and constructor and rehydration reject array references, preventing cyclic or externally mutable arguments from entering cached route definitions.
When generating a route cache with var_export(), set serialize_precision=-1 for the export and restore the prior setting in a finally block. Under that export contract, rehydration preserves exact float identity. Lower precision may alter float literals before __set_state() is called.
Native serialization does not depend on the export contract: __serialize() stores the arguments as a compact canonical string with exact IEEE-754 float bit strings, built on demand, so unserialize() round trips preserve float identity under any serialize_precision.
Non-finite float identity
signature() treats all NAN values as one canonical identity, because distinct NaN payloads cannot survive a var_export() cache round trip (NAN is exported as a single literal). INF and -INF remain distinct identities and round-trip exactly, including 0.0 versus -0.0. Two specifications whose arguments differ only in NaN payload are therefore equivalent by design; 1.2.1 briefly distinguished NaN payloads in signatures, which is no longer guaranteed. signature() encodes the (service, factory, arguments) tuple with serialize() under a temporarily forced serialize_precision=-1 (restored in a finally block): it is independent of the ambient setting, computed on demand, and holds no persistent per-instance or global state.
Versioning
The 1.x series follows Semantic Versioning.
Breaking changes to public contracts are reserved for the next major version.