Introducing Miniroute
Welcome to Miniroute!
Directory
- About
- Requirements
- Licensing
- Changelog
About
Miniroute is a PHP minimalist attribute router that provides integrated support for middleware unwrapping. The project retains a single php unit dependency tree, and otherwise is maintained as-is utilizing PHP's core library and toolkit. To provide a simple reflection kernel that changes HTTP requests into controller executions, the kernel relies on typical Model View Controller design constraints which are common in most web development paradigms. The pattern is inspired by Spaties Implementation for Laravel's attribute routing, while providing a more functional programming approach to how attribute routing ultimately resolves to an HTTP response.
As of writing (8/29/2026) the application is in the early design stages and is being actively integrated into brett-parson.com. The kernel itself relies on several simple assumptions:
- A controller resolver will provide the resolution strategies for the kernel to utilize
- Controller objects are an application concern
- Request and Response objects are an application concern
- Middleware definitions are an application concern
These simple application integration points allow for simple and terse definitions of middleware and controllers, where route definitions and service calls wrapped by declarative middleware can coincide with a single method definition.
<?php
#[Get('/toy/guarded')]
#[Middleware(ToyGuardMiddleware::class)]
public function guarded(RequestInterface $request): ResponseInterface
{
return ToyResponse::html('Guarded', '<h1>/toy/guarded</h1>');
}
The middleware may simply match the path provided by the request and block all guarded methods with an HTTP 403 response:
<?php
final class ToyGuardMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly string $name
) {
}
public function handle(RequestInterface $request, callable $next): ResponseInterface
{
if (($request->path() === '/toy/guarded') && (($_GET['block'] ?? '') === '1')) {
return ToyResponse::html(403, 'Forbidden', '<p>Blocked by ' . $this->name . ' before the controller ran.</p>');
}
return $next($request)->withHeader('X-Toy-Guard', 'pass');
}
}
In some cases we may intend on applying the same middleware for entire partitions of our routing directories. Perhaps the user always want admin/ routes to apply the ToyAdminMiddleware handler in their admin controllers with admin route definitions. Instead of providing Middleware Attributes for every single controller method, the user may instead simply wrap the route directly:
<?php
$router = new Router($resolver);
$router->group('/admin', [ToyAdminMiddleware::class]);
$router->registerController(ToyController::class);
Which provides declarative middleware grouping to all /admin routes defined within ToyController.
The resolver is the most confusing application layer necessary to compose the user's router. The controller router requires the resolver to accept controller objects and compose them into a list the router can rely on for registering the routes. As of writing (8/29/2026) these patterns are still experimental and being worked on to provide developer friendly patterns that don't require reflection magic to compose controllers into resolvable routes.
The resolver defines the factory provision of the controller list, the definition of which is determined by the application itself. Strictly, the set-resolve pattern may opt for hard coding, or it may be more elaborately composed, but so long as it provisions the controller object for reflection the router can reason over the defined attributes:
<?php
final class ToyResolver implements ControllerResolverInterface
{
/** @var array<string, callable(): object> */
private array $factories = [];
public function set(string $class, callable $factory): void
{
$this->factories[$class] = $factory;
}
public function resolve(string $class): object
{
if (isset($this->factories[$class])) {
return ($this->factories[$class])();
}
if (class_exists($class)) {
return new $class();
}
throw new RuntimeException("ToyResolver: cannot resolve {$class}");
}
}
After which the application may freely describe how to set the resolver pattern within it's own index:
<?php
$resolver = new ToyResolver();
$resolver->set(ToyController::class, static fn (): ToyController => new ToyController());
$router = new Router($resolver);
Thereby providing the router with the controllers.
Finally, the requester of the application may provide a route that does not exist, or faults the router. To tolerate these faults, the router throws exceptions in such cases, which the application may capture and handle:
<?php
try {
$response = $router->dispatch(new ToyRequest($request));
} catch (RouteNotFoundException) {
return ToyResponse::html(404, 'Not Found', '<p>Could not find requested resource</p>');
}
Requirements
Native PHP attributes are required to run. PHP >= 8.2.
Licensing
See MIT Licensing information for details.
Change Log
I will attempt to maintain major updates here, but users will want to refer to the git changelog that tracks the minutiae.
v0.1.0
- Attribute route declaration (`Get`, `Post`, `Put`, `Patch`, `Delete`) compiled by a reflection-based `RouteLoader`.
- Deterministic matching: literal segments beat `{parameter}` segments at the same position, so registration order never matters.
- Onion middleware pipeline (`MiddlewarePipeline`) with `Middleware` attributes on classes and methods, plus router-level `group()` prefix middleware with optional method restriction.
- Controller resolution seam (`ControllerResolverInterface`) so apps plug in their own container wiring.
- Thin HTTP contracts (`RequestInterface`, `ResponseInterface`) that application HTTP objects implement.
- Duplicate route detection at registration (`RouteRegistrationException`).
- Unit test suite covering loader, matcher, pipeline, and router.