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 SunriseHttpMessageResponseFactory; use SunriseHttpRouterRouteCollector; use SunriseHttpRouterRouter; use SunriseHttpServerRequestServerRequestFactory; use function SunriseHttpRouteremit; $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 SunriseHttpRouterLoaderConfigLoader; use SunriseHttpRouterRouter; $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 SunriseHttpRouterRouteCollector $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 AppHttpControllerHomeController());

Please note that since version 2.10.0 you can refer to the request handler in different ways.

/** @var SunriseHttpRouterRouteCollector $this */ $this->get('home', '/', function ($request) { return (new ResponseFactory)->createJsonResponse(200); }); $this->get('home', '/', AppHttpControllerHomeController::class, [ AppHttpMiddlewareFooMiddleware::class, AppHttpMiddlewareBarMiddleware::class, ]); $this->get('home', '/', [AppHttpControllerHomeController::class, 'index'], [ AppHttpMiddlewareFooMiddleware::class, AppHttpMiddlewareBarMiddleware::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 DoctrineCommonAnnotationsAnnotationRegistry; use SunriseHttpRouterLoaderDescriptorLoader; use SunriseHttpRouterRouter; // 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(AppHttpControllerFooController::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 SunriseHttpRouterAnnotation as Mapping; #[MappingPrefix('/api/v1')] #[MappingMiddleware(SomeMiddleware::class)] class SomeController { #[MappingRoute('foo', path: '/foo')] public function foo() { // will be available at: /api/v1/foo } #[MappingRoute('bar', path: '/bar')] public function bar() { // will be available at: /api/v1/bar } }

Without loading strategy

use AppControllerHomeController; use SunriseHttpRouterRouteCollector; use SunriseHttpRouterRouter; $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 SunriseHttpMessageResponseFactory; use SunriseHttpRouterExceptionMethodNotAllowedException; use SunriseHttpRouterExceptionRouteNotFoundException; use SunriseHttpRouterMiddlewareCallableMiddleware; use SunriseHttpRouterRequestHandlerCallableRequestHandler; use SunriseHttpRouterRouteCollector; use SunriseHttpRouterRouter; use SunriseHttpServerRequestServerRequestFactory; use function SunriseHttpRouteremit; $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 PsrContainerContainerInterface $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 PsrContainerContainerInterface $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 PsrContainerContainerInterface $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 PsrSimpleCacheCacheInterface $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={ * "AppMiddlewareCorsMiddleware", * "AppMiddlewareApiAuthMiddleware", * }, * 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 SunriseHttpRouterAnnotationRoute; #[Route( name: 'api_v1_entry_update', path: '/api/v1/entry/{id<@uuid>}(/{optionalAttribute})', methods: ['PATCH'], )] final class EntryUpdateRequestHandler implements RequestHandlerInterface

Full attribute

use SunriseHttpRouterAnnotationRoute; #[Route( name: 'api_v1_entry_update', host: 'api.host', path: '/api/v1/entry/{id<@uuid>}(/{optionalAttribute})', methods: ['PATCH'], middlewares: [ AppMiddlewareCorsMiddleware::class, AppMiddlewareApiAuthMiddleware::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 SunriseHttpRouterAnnotationHost; #[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 SunriseHttpRouterMiddlewareJsonPayloadDecodingMiddleware; $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(SunriseHttpRouterRouteInterface::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... }, [ AppHttpMiddlewareBar::class, // resolvable middlewares... ]) ->addPrefix('/v1') // add the prefix to the group... ->prependMiddleware(...); // add the middleware(s) to the group... }, [ AppHttpMiddlewareFoo::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): SunriseHttpRouterRouter::$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 SunriseHttpRouterCommandRouteListCommand; new RouteListCommand($router);

Events

Available from version 2.13

composer require symfony/event-dispatcher

use SunriseHttpRouterEventRouteEvent; use SymfonyComponentEventDispatcherEventDispatcher; $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

https://www.php-fig.org/psr/psr-7/ https://www.php-fig.org/psr/psr-15/ https://github.com/sunrise-php/awesome-skeleton https://github.com/middlewares

版权声明:

1、该文章(资料)来源于互联网公开信息,我方只是对该内容做点评,所分享的下载地址为原作者公开地址。
2、网站不提供资料下载,如需下载请到原作者页面进行下载。
3、本站所有内容均由合作方或网友上传,本站不对文档的完整性、权威性及其观点立场正确性做任何保证或承诺!文档内容仅供研究参考学习用!
4、如文档内容存在违规,或者侵犯商业秘密、侵犯著作权等,请点击“违规举报”。