Description
The example uses other sunrise packages, but you can use, for example, zend/diactoros or any other.
HTTP router alternatives and similar libraries
Based on the "Routers" category.
Alternatively, view http-router alternatives based on common mentions on social networks and blogs.
-
AltoRouter
PHP routing class. Lightweight yet flexible. Supports REST, dynamic and reversed routing. -
Route
Fast PSR-7 based routing and dispatch component including PSR-15 middleware, built on top of FastRoute.
CodeRabbit: AI Code Reviews for Developers
* Code Quality Rankings and insights are calculated and provided by Lumnify.
They vary from L1 to L5 with "L5" being the highest.
Do you think we are missing an alternative of HTTP router or a related project?
README
HTTP router for PHP 7.1+ based on PSR-7 and PSR-15 with support for annotations/attributes and OpenAPI (Swagger) Specification
psr router, router with annotations, router with attributes, php router.
Installation
composer require 'sunrise/http-router:^2.15'
Support for OpenAPI (Swagger) Specification (optional)
composer require 'sunrise/http-router-openapi:^2.0'
More details can be found here: sunrise/http-router-openapi.
QuickStart
This example uses other sunrise packages, but you can use e.g. zend/diactoros
or any other.
composer require sunrise/http-message sunrise/http-server-request
use Sunrise\Http\Message\ResponseFactory;
use Sunrise\Http\Router\RouteCollector;
use Sunrise\Http\Router\Router;
use Sunrise\Http\ServerRequest\ServerRequestFactory;
use function Sunrise\Http\Router\emit;
$collector = new RouteCollector();
// PSR-15 request handler (optimal performance):
$collector->get('home', '/', new HomeRequestHandler());
// or you can use an anonymous function as your request handler:
$collector->get('home', '/', function ($request) {
return (new ResponseFactory)->createResponse(200);
});
// or you can use the name of a class that implements PSR-15:
$collector->get('home', '/', HomeRequestHandler::class);
// or you can use a class method name as your request handler:
// (note that such a class mayn't implement PSR-15)
$collector->get('home', '/', [HomeRequestHandler::class, 'index']);
// most likely you will need to use PSR-11 container:
// (note that only named classes will be pulled from such a container)
$collector->setContainer($container);
$router = new Router();
$router->addRoute(...$collector->getCollection()->all());
$request = ServerRequestFactory::fromGlobals();
$response = $router->handle($request);
emit($response);
Examples of using
Study sunrise/awesome-skeleton to understand how this can be used.
Strategy for loading routes from configs
Please note that since version 2.10.0 class
ConfigLoader
must be used.
use Sunrise\Http\Router\Loader\ConfigLoader;
use Sunrise\Http\Router\Router;
$loader = new ConfigLoader();
// set container if necessary...
$loader->setContainer($container);
// attach configs...
$loader->attach('routes/api.php');
$loader->attach('routes/admin.php');
$loader->attach('routes/public.php');
// or attach a directory...
// [!] available from version 2.2
$loader->attach('routes');
// or attach an array...
// [!] available from version 2.4
$loader->attachArray([
'routes/api.php',
'routes/admin.php',
'routes/public.php',
]);
// install container if necessary...
$loader->setContainer($container);
$router = new Router();
$router->load($loader);
// if the router matching should be isolated for top middlewares...
// for example for error handling...
// [!] available from version 2.8
$response = $router->run($request);
// if the router is used as a request handler
$response = $router->handle($request);
// if the router is used as middleware
$response = $router->process($request, $handler);
/** @var Sunrise\Http\Router\RouteCollector $this */
$this->get('home', '/', new CallableRequestHandler(function ($request) {
return (new ResponseFactory)->createJsonResponse(200);
}));
// or using a direct reference to a request handler...
$this->get('home', '/', new App\Http\Controller\HomeController());
Please note that since version 2.10.0 you can refer to the request handler in different ways.
/** @var Sunrise\Http\Router\RouteCollector $this */
$this->get('home', '/', function ($request) {
return (new ResponseFactory)->createJsonResponse(200);
});
$this->get('home', '/', App\Http\Controller\HomeController::class, [
App\Http\Middleware\FooMiddleware::class,
App\Http\Middleware\BarMiddleware::class,
]);
$this->get('home', '/', [App\Http\Controller\HomeController::class, 'index'], [
App\Http\Middleware\FooMiddleware::class,
App\Http\Middleware\BarMiddleware::class,
]);
Strategy for loading routes from descriptors (annotations or attributes)
Install the doctrine/annotations package if you will be use annotations:
composer require doctrine/annotations
Please note that since version 2.10.0 class
DescriptorLoader
must be used.Please note that since version 2.10.0 you can bind the @Rote() annotation to a class methods.
use Doctrine\Common\Annotations\AnnotationRegistry;
use Sunrise\Http\Router\Loader\DescriptorLoader;
use Sunrise\Http\Router\Router;
// necessary if you will use annotations (annotations isn't attributes)...
AnnotationRegistry::registerLoader('class_exists');
$loader = new DescriptorLoader();
// set container if necessary...
$loader->setContainer($container);
// attach a directory with controllers...
$loader->attach('src/Controller');
// or attach an array
// [!] available from version 2.4
$loader->attachArray([
'src/Controller',
'src/Bundle/BundleName/Controller',
]);
// or attach a class only
// [!] available from 2.10 version.
$loader->attach(App\Http\Controller\FooController::class);
$router = new Router();
$router->load($loader);
// if the router matching should be isolated for top middlewares...
// for example for error handling...
// [!] available from version 2.8
$response = $router->run($request);
// if the router is used as a request handler
$response = $router->handle($request);
// if the router is used as middleware
$response = $router->process($request, $handler);
use Sunrise\Http\Router\Annotation as Mapping;
#[Mapping\Prefix('/api/v1')]
#[Mapping\Middleware(SomeMiddleware::class)]
class SomeController {
#[Mapping\Route('foo', path: '/foo')]
public function foo() {
// will be available at: /api/v1/foo
}
#[Mapping\Route('bar', path: '/bar')]
public function bar() {
// will be available at: /api/v1/bar
}
}
Without loading strategy
use App\Controller\HomeController;
use Sunrise\Http\Router\RouteCollector;
use Sunrise\Http\Router\Router;
$collector = new RouteCollector();
// set container if necessary...
$collector->setContainer($container);
$collector->get('home', '/', new HomeController());
$router = new Router();
$router->addRoute(...$collector->getCollection()->all());
// if the router matching should be isolated for top middlewares...
// for example for error handling...
// [!] available from version 2.8
$response = $router->run($request);
// if the router is used as a request handler
$response = $router->handle($request);
// if the router is used as middleware
$response = $router->process($request, $handler);
Error handling example
use Sunrise\Http\Message\ResponseFactory;
use Sunrise\Http\Router\Exception\MethodNotAllowedException;
use Sunrise\Http\Router\Exception\RouteNotFoundException;
use Sunrise\Http\Router\Middleware\CallableMiddleware;
use Sunrise\Http\Router\RequestHandler\CallableRequestHandler;
use Sunrise\Http\Router\RouteCollector;
use Sunrise\Http\Router\Router;
use Sunrise\Http\ServerRequest\ServerRequestFactory;
use function Sunrise\Http\Router\emit;
$collector = new RouteCollector();
$collector->get('home', '/', new CallableRequestHandler(function ($request) {
return (new ResponseFactory)->createJsonResponse(200);
}));
$router = new Router();
$router->addRoute(...$collector->getCollection()->all());
$router->addMiddleware(new CallableMiddleware(function ($request, $handler) {
try {
return $handler->handle($request);
} catch (MethodNotAllowedException $e) {
return (new ResponseFactory)->createResponse(405);
} catch (RouteNotFoundException $e) {
return (new ResponseFactory)->createResponse(404);
} catch (Throwable $e) {
return (new ResponseFactory)->createResponse(500);
}
}));
emit($router->run(ServerRequestFactory::fromGlobals()));
Work with PSR-11 container
Collector
$collector = new RouteCollector();
/** @var \Psr\Container\ContainerInterface $container */
// Pass DI container to the collector...
$collector->setContainer($container);
// Objects passed as strings will be initialized through the DI container...
$route = $collector->get('home', '/', HomeController::class, [
FooMiddleware::class,
BarMiddleware::class,
]);
Config loader
$loader = new ConfigLoader();
/** @var \Psr\Container\ContainerInterface $container */
// Pass DI container to the loader...
$loader->setContainer($container);
// All found objects which has been passed as strings will be initialized through the DI container...
$routes = $loader->load();
Descriptor loader
$loader = new DescriptorLoader();
/** @var \Psr\Container\ContainerInterface $container */
// Pass DI container to the loader...
$loader->setContainer($container);
// All found objects will be initialized through the DI container...
$routes = $loader->load();
Descriptors cache (PSR-16)
$loader = new DescriptorLoader();
/** @var \Psr\SimpleCache\CacheInterface $cache */
// Pass a cache to the loader...
$loader->setCache($cache);
Route Annotation Example
Minimal annotation view
/**
* @Route(
* name="api_v1_entry_update",
* path="/api/v1/entry/{id<@uuid>}(/{optionalAttribute})",
* methods={"PATCH"},
* )
*/
final class EntryUpdateRequestHandler implements RequestHandlerInterface
Full annotation
/**
* @Route(
* name="api_v1_entry_update",
* host="api.host",
* path="/api/v1/entry/{id<@uuid>}(/{optionalAttribute})",
* methods={"PATCH"},
* middlewares={
* "App\Middleware\CorsMiddleware",
* "App\Middleware\ApiAuthMiddleware",
* },
* attributes={
* "optionalAttribute": "defaultValue",
* },
* summary="Updates an entry by UUID",
* description="Here you can describe the method in more detail...",
* tags={"api", "entry"},
* priority=0,
* )
*/
final class EntryUpdateRequestHandler implements RequestHandlerInterface
One method only
/**
* @Route(
* name="home",
* path="/",
* method="GET",
* )
*/
Route Attribute Example
Minimal attribute view
use Sunrise\Http\Router\Annotation\Route;
#[Route(
name: 'api_v1_entry_update',
path: '/api/v1/entry/{id<@uuid>}(/{optionalAttribute})',
methods: ['PATCH'],
)]
final class EntryUpdateRequestHandler implements RequestHandlerInterface
Full attribute
use Sunrise\Http\Router\Annotation\Route;
#[Route(
name: 'api_v1_entry_update',
host: 'api.host',
path: '/api/v1/entry/{id<@uuid>}(/{optionalAttribute})',
methods: ['PATCH'],
middlewares: [
\App\Middleware\CorsMiddleware::class,
\App\Middleware\ApiAuthMiddleware::class,
],
attributes: [
'optionalAttribute' => 'defaultValue',
],
summary: 'Updates an entry by UUID',
description: 'Here you can describe the method in more detail...',
tags: ['api', 'entry'],
priority: 0,
)]
final class EntryUpdateRequestHandler implements RequestHandlerInterface
Additional annotations
use Sunrise\Http\Router\Annotation\Host;
#[Host('admin')]
#[Prefix('/api/v1')]
#[Postfix('.json')]
#[Middleware(SomeMiddleware::class)]
final class SomeController
{
#[Route('foo', '/foo')]
public function foo(ServerRequestInterface $request) : ResponseInterface
{
// this action will be available at:
// http://admin.host/api/v1/foo.json
//
// this can be handy to reduce code duplication...
}
}
Useful to know
JSON-payload decoding
use Sunrise\Http\Router\Middleware\JsonPayloadDecodingMiddleware;
$router->addMiddleware(new JsonPayloadDecodingMiddleware());
Get a route by name
// checks if a route is exists
$router->hasRoute('foo');
// gets a route by name
$router->getRoute('foo');
Get a current route
Through Router
Available from version 2.12.
$router->getMatchedRoute();
Through Request
Available from version 1.x, but wasn't documented before...
$request->getAttribute('@route');
// or
$request->getAttribute(\Sunrise\Http\Router\RouteInterface::ATTR_ROUTE);
Through Event
Available from version 2.13.
$eventDispatcher->addListener(RouteEvent::NAME, function (RouteEvent $event) {
$event->getRoute();
});
Generation a route URI
$uri = $router->generateUri('route.name', [
'attribute' => 'value',
], true);
Run a route
$response = $router->getRoute('route.name')->handle($request);
Route grouping
Example for annotations here.
$collector->group(function ($collector) {
$collector->group(function ($collector) {
$collector->group(function ($collector) {
$collector->get('api.entry.read', '/{id<\d+>}', ...)
->addMiddleware(...); // add the middleware(s) to the route...
})
->addPrefix('/entry') // add the prefix to the group...
->prependMiddleware(...); // add the middleware(s) to the group...
}, [
App\Http\Middleware\Bar::class, // resolvable middlewares...
])
->addPrefix('/v1') // add the prefix to the group...
->prependMiddleware(...); // add the middleware(s) to the group...
}, [
App\Http\Middleware\Foo::class, // resolvable middlewares...
])
->addPrefix('/api') // add the prefix to the group...
->prependMiddleware(...); // add the middleware(s) to the group...
Route patterns
$collector->get('api.entry.read', '/api/v1/entry/{id<\d+>}(/{optional<\w+>})');
Global route patterns
// @uuid pattern
$collector->get('api.entry.read', '/api/v1/entry/{id<@uuid>}');
// @slug pattern
$collector->get('api.entry.read', '/api/v1/entry/{slug<@slug>}');
// Custom patterns (available from version 2.9.0):
\Sunrise\Http\Router\Router::$patterns['@id'] = '[1-9][0-9]*';
// Just use the custom pattern...
$collector->get('api.entry.read', '/api/v1/entry/{id<@id>}');
It is better to set patterns through the router:
// available since version 2.11.0
$router->addPatterns([
'@id' => '[1-9][0-9]*',
]);
...or through the router's builder:
// available since version 2.11.0
$builder->setPatterns([
'@id' => '[1-9][0-9]*',
]);
Hosts (available from version 2.6.0)
Note: if you don't assign a host for a route, it will be available on any hosts!
// move the hosts table into the settings...
$router->addHost('public.host', 'www.example.com', ...);
$router->addHost('admin.host', 'secret.example.com', ...);
$router->addHost('api.host', 'api.example.com', ...);
// ...or:
$router->addHosts([
'public.host' => ['www.example.com', ...],
...
]);
// the route will available only on the `secret.example.com` host...
$route->setHost('admin.host');
// routes in the group will available on the `secret.example.com` host...
$collector->group(function ($collector) {
// some code...
})
->setHost('admin.host');
You can resolve the hostname since version 2.14.0 as follows:
$router->addHost('admin', 'www1.admin.example.com', 'www2.admin.example.com');
$router->resolveHostname('www1.admin.example.com'); // return "admin"
$router->resolveHostname('www2.admin.example.com'); // return "admin"
$router->resolveHostname('unknown'); // return null
Also you can get all routes by hostname:
$router->getRoutesByHostname('www1.admin.example.com');
Route Holder
$route->getHolder(); // return Reflector (class, method or function)
The router builder
$router = (new RouterBuilder)
->setEventDispatcher(...) // null or use to symfony/event-dispatcher...
->setContainer(...) // null or PSR-11 container instance...
->setCache(...) // null or PSR-16 cache instance... (only for descriptor loader)
->setCacheKey(...) // null or string... (only for descriptor loader)
->useConfigLoader([]) // array with files or directory with files...
->useDescriptorLoader([]) // array with classes or directory with classes...
->setHosts([]) //
->setMiddlewares([]) // array with middlewares...
->setPatterns([]) // available since version 2.11.0
->build();
CLI commands
use Sunrise\Http\Router\Command\RouteListCommand;
new RouteListCommand($router);
Events
Available from version 2.13
composer require symfony/event-dispatcher
use Sunrise\Http\Router\Event\RouteEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addListener(RouteEvent::NAME, function (RouteEvent $event) {
// gets the matched route:
$event->getRoute();
// gets the current request:
$event->getRequest();
// overrides the current request:
$event->setRequest(ServerRequestInterface $request);
});
$router->setEventDispatcher($eventDispatcher);
Test run
composer test
Useful links
*Note that all licence references and agreements mentioned in the HTTP router README section above
are relevant to that project's source code only.