Awesome Open Source
Awesome Open Source

HTTP router for PHP 7.1+ (incl. PHP 8 with attributes) based on PSR-7 and PSR-15 with support for annotations and OpenAPI (Swagger) Specification

Build Status Code Coverage Scrutinizer Code Quality Total Downloads Latest Stable Version License


Installation

composer require 'sunrise/http-router:^2.11'

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

Useful to know

Generation a route URI

$uri = $router->generateUri('route.name', [
    'attribute' => 'value',
], true);

Run a route

$response = $router->getRoute('route.name')->handle($request);

Route grouping

$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>}');

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');

// 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');

The router builder

$router = (new RouterBuilder)
    ->setContainer(null) // null or PSR-11 container instance...
    ->setCache(null) // null or PSR-16 cache instance... (only for descriptor loader)
    ->setCacheKey(null) // 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...
    ->build();

CLI commands

use Sunrise\Http\Router\Command\RouteListCommand;

new RouteListCommand($router);

Test run

composer test

Useful links


Get A Weekly Email With Trending Projects For These Topics
No Spam. Unsubscribe easily at any time.
Php (404,388
Http (5,603
Php7 (4,469
Swagger (2,614
Middleware (2,498
Router (2,085
Benchmark (1,866
Openapi (1,412
Php Library (1,383
Annotations (1,320
Openapi3 (449
Php8 (419
Psr 7 (373
Psr 15 (252
Related Projects