diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index 706d6406..00000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-# PHP CircleCI 2.0 configuration file
-#
-# Check https://circleci.com/docs/2.0/language-php/ for more details
-#
-version: 2
-jobs:
- php71:
- docker:
- - image: cimg/php:7.1
- steps:
- - checkout
- - run: php -v
- - run: composer install --no-interaction
- - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text
- php72:
- docker:
- - image: cimg/php:7.2
- steps:
- - checkout
- - run: php -v
- - run: composer install --no-interaction
- - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text
- php73:
- docker:
- - image: cimg/php:7.3
- steps:
- - checkout
- - run: php -v
- - run: composer install --no-interaction
- - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text
- php74:
- docker:
- - image: cimg/php:7.4
- steps:
- - checkout
- - run: php -v
- - run: composer install --no-interaction
- - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text
- php80:
- docker:
- - image: cimg/php:8.0
- steps:
- - checkout
- - run: php -v
- - run: composer install --no-interaction
- - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text
- php81:
- docker:
- - image: cimg/php:8.1
- steps:
- - checkout
- - run: php -v
- - run: composer install --no-interaction
- - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text
-workflows:
- version: 2
- build:
- jobs:
- - php71
- - php72
- - php73
- - php74
- - php80
- - php81
diff --git a/.editorconfig b/.editorconfig
index 91aebb87..0beac55a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -11,5 +11,5 @@ indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
-[*.yml]
+[*{.json,.yml}]
indent_size = 2
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 1e976c3a..52a1ded1 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,7 +1,7 @@
# These are supported funding model platforms
github: fenric # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
-patreon: # Replace with a single Patreon username
+patreon: afenric
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: "packagist/sunrise/http-router" # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
index 1e6a96fc..6f32340f 100644
--- a/.scrutinizer.yml
+++ b/.scrutinizer.yml
@@ -1,16 +1,24 @@
build:
- environment:
- php:
- version: '8.0'
+ image: default-bionic
nodes:
analysis:
+ environment:
+ php: 8.2
tests:
override:
- php-scrutinizer-run
coverage:
+ environment:
+ php: 8.2
tests:
override:
- command: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-clover coverage.xml
coverage:
file: coverage.xml
format: clover
+ php81:
+ environment:
+ php: 8.1
+ tests:
+ override:
+ - command: php vendor/bin/phpunit
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4cc563b..b1a1074b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,18 +1 @@
-# v2.16.0
-
-* New method: `Router::hasRoute(string):bool`.
-
-## v2.15.0
-
-* New middleware: `Sunrise\Http\Router\Middleware\JsonPayloadDecodingMiddleware`.
-
-## v2.14.0
-
-* New method: `Route::getHolder():Reflector`;
-* New method: `Router::resolveHostname(string):?string`;
-* New method: `Router::getRoutesByHostname(string):array`;
-* New method: `RouterBuilder::setEventDispatcher(?EventDispatcherInterface):void`.
-
-## v2.13.0
-
-* Supports for events using the `symfony/event-dispatcher`.
+# v3.0.0
diff --git a/README.md b/README.md
index a20787bf..d33ac903 100644
--- a/README.md
+++ b/README.md
@@ -1,714 +1 @@
-# 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**.
-
-[![Build Status](https://circleci.com/gh/sunrise-php/http-router.svg?style=shield)](https://circleci.com/gh/sunrise-php/http-router)
-[![Code Coverage](https://scrutinizer-ci.com/g/sunrise-php/http-router/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-router/?branch=master)
-[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sunrise-php/http-router/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-router/?branch=master)
-[![Total Downloads](https://poser.pugx.org/sunrise/http-router/downloads?format=flat)](https://packagist.org/packages/sunrise/http-router)
-[![Latest Stable Version](https://poser.pugx.org/sunrise/http-router/v/stable?format=flat)](https://packagist.org/packages/sunrise/http-router)
-[![License](https://poser.pugx.org/sunrise/http-router/license?format=flat)](https://packagist.org/packages/sunrise/http-router)
-
----
-
-## Installation
-
-```bash
-composer require 'sunrise/http-router:^2.15'
-```
-
-## Support for OpenAPI (Swagger) Specification (optional)
-
-```bash
-composer require 'sunrise/http-router-openapi:^2.0'
-```
-
-More details can be found here: [sunrise/http-router-openapi](https://github.com/sunrise-php/http-router-openapi).
-
-## QuickStart
-
-This example uses other sunrise packages, but you can use e.g. `zend/diactoros` or any other.
-
-```bash
-composer require sunrise/http-message sunrise/http-server-request
-```
-
-```php
-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](https://github.com/sunrise-php/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.
-
-```php
-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);
-```
-
-```php
-/** @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.
-
-```php
-/** @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](https://github.com/doctrine/annotations) package if you will be use annotations:
-
-```bash
-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.
-
-```php
-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);
-```
-
-```php
-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
-
-```php
-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
-
-```php
-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
-
-```php
-$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
-
-```php
-$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
-
-```php
-$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)
-
-```php
-$loader = new DescriptorLoader();
-
-/** @var \Psr\SimpleCache\CacheInterface $cache */
-
-// Pass a cache to the loader...
-$loader->setCache($cache);
-```
-
-#### Route Annotation Example
-
-##### Minimal annotation view
-
-```php
-/**
- * @Route(
- * name="api_v1_entry_update",
- * path="/api/v1/entry/{id<@uuid>}(/{optionalAttribute})",
- * methods={"PATCH"},
- * )
- */
-final class EntryUpdateRequestHandler implements RequestHandlerInterface
-```
-
-##### Full annotation
-
-```php
-/**
- * @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
-
-```php
-/**
- * @Route(
- * name="home",
- * path="/",
- * method="GET",
- * )
- */
-```
-
-#### Route Attribute Example
-
-##### Minimal attribute view
-
-```php
-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
-
-```php
-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
-
-```php
-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
-
-```php
-use Sunrise\Http\Router\Middleware\JsonPayloadDecodingMiddleware;
-
-$router->addMiddleware(new JsonPayloadDecodingMiddleware());
-```
-
-### Get a route by name
-
-```php
-// 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.
-
-```php
-$router->getMatchedRoute();
-```
-
-#### Through Request
-
-> Available from version 1.x, but wasn't documented before...
-
-```php
-$request->getAttribute('@route');
-
-// or
-$request->getAttribute(\Sunrise\Http\Router\RouteInterface::ATTR_ROUTE);
-```
-
-#### Through Event
-
-> Available from version 2.13.
-
-```php
-$eventDispatcher->addListener(RouteEvent::NAME, function (RouteEvent $event) {
- $event->getRoute();
-});
-```
-
-### Generation a route URI
-
-```php
-$uri = $router->generateUri('route.name', [
- 'attribute' => 'value',
-], true);
-```
-
-### Run a route
-
-```php
-$response = $router->getRoute('route.name')->handle($request);
-```
-
-### Route grouping
-
-Example for annotations [here](#additional-annotations).
-
-```php
-$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
-
-```php
-$collector->get('api.entry.read', '/api/v1/entry/{id<\d+>}(/{optional<\w+>})');
-```
-
-##### Global route patterns
-
-```php
-// @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:
-
-```php
-// available since version 2.11.0
-$router->addPatterns([
- '@id' => '[1-9][0-9]*',
-]);
-```
-
-...or through the router's builder:
-
-```php
-// 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!
-
-```php
-// 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:
-
-```php
-$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:
-
-```php
-$router->getRoutesByHostname('www1.admin.example.com');
-```
-
-### Route Holder
-
-```php
-$route->getHolder(); // return Reflector (class, method or function)
-```
-
-### The router builder
-
-```php
-$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
-
-```php
-use Sunrise\Http\Router\Command\RouteListCommand;
-
-new RouteListCommand($router);
-```
-
-### Events
-
-> Available from version 2.13
-
-```bash
-composer require symfony/event-dispatcher
-```
-
-```php
-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
-
-```bash
-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
+# Very soon...
diff --git a/benchmarks/.gitkeep b/benchmarks/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/composer.json b/composer.json
index 304c6856..4bffb653 100644
--- a/composer.json
+++ b/composer.json
@@ -1,75 +1,78 @@
{
- "name": "sunrise/http-router",
- "homepage": "https://github.com/sunrise-php/http-router",
- "description": "HTTP router for PHP 7.1+ based on PSR-7 and PSR-15 with support for annotations/attributes and OpenAPI (Swagger) Specification",
- "license": "MIT",
- "keywords": [
- "fenric",
- "sunrise",
- "http",
- "router",
- "request-handler",
- "middlewares",
- "annotations",
- "attributes",
- "openapi",
- "swagger",
- "psr-7",
- "psr-15",
- "php7",
- "php8"
- ],
- "authors": [
- {
- "name": "Anatoly Fenric",
- "email": "afenric@gmail.com",
- "homepage": "https://github.com/fenric"
- }
+ "name": "sunrise/http-router",
+ "homepage": "https://github.com/sunrise-php/http-router",
+ "description": "HTTP router for PHP 8.1+ based on PSR-7 and PSR-15",
+ "license": "MIT",
+ "keywords": [
+ "fenric",
+ "sunrise",
+ "http",
+ "router",
+ "openapi",
+ "swagger",
+ "psr-7",
+ "psr-15"
+ ],
+ "authors": [
+ {
+ "name": "Anatoly Nekhay",
+ "email": "afenric@gmail.com",
+ "homepage": "https://github.com/fenric"
+ }
+ ],
+ "require": {
+ "php": ">=8.1",
+ "fig/http-message-util": "^1.1",
+ "psr/container": "^1.0 || ^2.0",
+ "psr/event-dispatcher": "^1.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0 || ^2.0",
+ "psr/http-server-handler": "^1.0",
+ "psr/http-server-middleware": "^1.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "php-di/php-di": "^7.0",
+ "phpstan/phpstan": "^1.12",
+ "phpunit/phpunit": "^10.5",
+ "squizlabs/php_codesniffer": "^3.10",
+ "sunrise/http-message": "^3.2",
+ "sunrise/hydrator": "^3.13",
+ "symfony/console": "^6.4",
+ "symfony/validator": "^6.4",
+ "vimeo/psalm": "^5.26"
+ },
+ "autoload": {
+ "files": [
+ "src/functions/emit.php"
],
- "require": {
- "php": "^7.1|^8.0",
- "fig/http-message-util": "^1.1",
- "psr/container": "^1.0",
- "psr/http-message": "^1.0",
- "psr/http-server-handler": "^1.0",
- "psr/http-server-middleware": "^1.0",
- "psr/simple-cache": "^1.0"
- },
- "require-dev": {
- "phpunit/phpunit": "7.5.20|9.5.0",
- "sunrise/coding-standard": "1.0.0",
- "sunrise/http-factory": "2.0.0",
- "doctrine/annotations": "^1.6",
- "symfony/console": "^4.4",
- "symfony/event-dispatcher": "^4.4"
- },
- "autoload": {
- "files": [
- "functions/emit.php",
- "functions/path_build.php",
- "functions/path_match.php",
- "functions/path_parse.php",
- "functions/path_plain.php",
- "functions/path_regex.php"
- ],
- "psr-4": {
- "Sunrise\\Http\\Router\\": "src/"
- }
- },
- "autoload-dev": {
- "psr-4": {
- "Sunrise\\Http\\Router\\Tests\\": "tests/"
- }
- },
- "scripts": {
- "test": [
- "phpcs",
- "psalm",
- "XDEBUG_MODE=coverage phpunit --coverage-text --colors=always"
- ],
- "build": [
- "phpdoc -d src/ -t phpdoc/",
- "XDEBUG_MODE=coverage phpunit --coverage-html coverage/"
- ]
+ "psr-4": {
+ "Sunrise\\Http\\Router\\": "src/"
}
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Sunrise\\Http\\Router\\Tests\\": "tests/"
+ }
+ },
+ "scripts": {
+ "phpcs": "phpcs --colors",
+ "psalm": "psalm --no-cache",
+ "phpstan": "phpstan analyse src --level=9 --memory-limit=-1",
+ "phpunit": "XDEBUG_MODE=coverage phpunit --coverage-text --colors=always",
+ "test": [
+ "@phpcs",
+ "@psalm",
+ "@phpstan",
+ "@phpunit"
+ ],
+ "build": [
+ "phpdoc -d src/ -t phpdoc/",
+ "XDEBUG_MODE=coverage phpunit --coverage-html coverage/"
+ ]
+ },
+ "config": {
+ "sort-packages": true
+ }
}
diff --git a/functions/path_build.php b/functions/path_build.php
deleted file mode 100644
index d7557b9a..00000000
--- a/functions/path_build.php
+++ /dev/null
@@ -1,84 +0,0 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
- * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
- * @link https://github.com/sunrise-php/http-router
- */
-
-namespace Sunrise\Http\Router;
-
-/**
- * Import classes
- */
-use Sunrise\Http\Router\Exception\InvalidAttributeValueException;
-use Sunrise\Http\Router\Exception\MissingAttributeValueException;
-
-/**
- * Import functions
- */
-use function preg_match;
-use function sprintf;
-use function str_replace;
-
-/**
- * Builds the given path using the given attributes
- *
- * If strict mode is enabled, each attribute value will be validated.
- *
- * @param string $path
- * @param array $attributes
- * @param bool $strict
- *
- * @return string
- *
- * @throws InvalidAttributeValueException
- * @throws MissingAttributeValueException
- */
-function path_build(string $path, array $attributes = [], bool $strict = false) : string
-{
- $result = $path;
- $matches = path_parse($path);
-
- foreach ($matches as $match) {
- // handle not required attributes...
- if (!isset($attributes[$match['name']])) {
- if (!$match['isOptional']) {
- $errmsg = '[%s] build error: no value given for the attribute "%s".';
-
- throw new MissingAttributeValueException(sprintf($errmsg, $path, $match['name']), [
- 'path' => $path,
- 'match' => $match,
- ]);
- }
-
- $result = str_replace($match['withParentheses'], '', $result);
-
- continue;
- }
-
- $replacement = (string) $attributes[$match['name']];
-
- // validate the given attributes values...
- if ($strict && isset($match['pattern'])) {
- if (!preg_match('#^' . $match['pattern'] . '$#u', $replacement)) {
- $errmsg = '[%s] build error: the given value for the attribute "%s" does not match its pattern.';
-
- throw new InvalidAttributeValueException(sprintf($errmsg, $path, $match['name']), [
- 'path' => $path,
- 'value' => $replacement,
- 'match' => $match,
- ]);
- }
- }
-
- $result = str_replace($match['raw'], $replacement, $result);
- }
-
- $result = str_replace(['(', ')'], '', $result);
-
- return $result;
-}
diff --git a/functions/path_match.php b/functions/path_match.php
deleted file mode 100644
index 2142c96c..00000000
--- a/functions/path_match.php
+++ /dev/null
@@ -1,44 +0,0 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
- * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
- * @link https://github.com/sunrise-php/http-router
- */
-
-namespace Sunrise\Http\Router;
-
-/**
- * Import functions
- */
-use function is_int;
-use function preg_match;
-
-/**
- * Compares the given path and the given subject...
- *
- * @param string $path
- * @param string $subject
- *
- * @return bool
- */
-function path_match(string $path, string $subject, &$attributes = null) : bool
-{
- $attributes = [];
-
- $regex = path_regex($path);
- if (!preg_match($regex, $subject, $matches)) {
- return false;
- }
-
- foreach ($matches as $key => $value) {
- if (!is_int($key) && '' !== $value) {
- $attributes[$key] = $value;
- }
- }
-
- return true;
-}
diff --git a/functions/path_parse.php b/functions/path_parse.php
deleted file mode 100644
index b95be62b..00000000
--- a/functions/path_parse.php
+++ /dev/null
@@ -1,286 +0,0 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
- * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
- * @link https://github.com/sunrise-php/http-router
- */
-
-namespace Sunrise\Http\Router;
-
-/**
- * Import classes
- */
-use Sunrise\Http\Router\Exception\InvalidPathException;
-
-/**
- * Import functions
- */
-use function sprintf;
-
-/**
- * Allowed characters for the first character of the subpattern name
- *
- * @var array
- */
-const CHARACTER_TABLE_FOR_FIRST_CHARACTER_OF_SUBPATTERN_NAME = [
- 'A' => 1, 'B' => 1, 'C' => 1, 'D' => 1, 'E' => 1, 'F' => 1, 'G' => 1, 'H' => 1, 'I' => 1, 'J' => 1,
- 'K' => 1, 'L' => 1, 'M' => 1, 'N' => 1, 'O' => 1, 'P' => 1, 'Q' => 1, 'R' => 1, 'S' => 1, 'T' => 1,
- 'U' => 1, 'V' => 1, 'W' => 1, 'X' => 1, 'Y' => 1, 'Z' => 1,
- 'a' => 1, 'b' => 1, 'c' => 1, 'd' => 1, 'e' => 1, 'f' => 1, 'g' => 1, 'h' => 1, 'i' => 1, 'j' => 1,
- 'k' => 1, 'l' => 1, 'm' => 1, 'n' => 1, 'o' => 1, 'p' => 1, 'q' => 1, 'r' => 1, 's' => 1, 't' => 1,
- 'u' => 1, 'v' => 1, 'w' => 1, 'x' => 1, 'y' => 1, 'z' => 1,
- '_' => 1,
-];
-
-/**
- * Allowed characters for the subpattern name
- *
- * @var array
- */
-const CHARACTER_TABLE_FOR_SUBPATTERN_NAME = [
- '0' => 1, '1' => 1, '2' => 1, '3' => 1, '4' => 1, '5' => 1, '6' => 1, '7' => 1, '8' => 1, '9' => 1,
- 'A' => 1, 'B' => 1, 'C' => 1, 'D' => 1, 'E' => 1, 'F' => 1, 'G' => 1, 'H' => 1, 'I' => 1, 'J' => 1,
- 'K' => 1, 'L' => 1, 'M' => 1, 'N' => 1, 'O' => 1, 'P' => 1, 'Q' => 1, 'R' => 1, 'S' => 1, 'T' => 1,
- 'U' => 1, 'V' => 1, 'W' => 1, 'X' => 1, 'Y' => 1, 'Z' => 1,
- 'a' => 1, 'b' => 1, 'c' => 1, 'd' => 1, 'e' => 1, 'f' => 1, 'g' => 1, 'h' => 1, 'i' => 1, 'j' => 1,
- 'k' => 1, 'l' => 1, 'm' => 1, 'n' => 1, 'o' => 1, 'p' => 1, 'q' => 1, 'r' => 1, 's' => 1, 't' => 1,
- 'u' => 1, 'v' => 1, 'w' => 1, 'x' => 1, 'y' => 1, 'z' => 1,
- '_' => 1,
-];
-
-/**
- * Parses the given path
- *
- * @param string $path
- *
- * @return array
- *
- * @throws InvalidPathException If the given path syntax isn't valid.
- */
-function path_parse(string $path) : array
-{
- // This will be useful for a long-running application,
- // for example if you use the RoadRunner server...
- static $cache = [];
-
- if (isset($cache[$path])) {
- return $cache[$path];
- }
-
- $attributes = [];
- $attributeIndex = -1;
- $attributePrototype = [
- 'raw' => null,
- 'withParentheses' => null,
- 'name' => null,
- 'pattern' => null,
- 'isOptional' => false,
- 'startPosition' => -1,
- 'endPosition' => -1,
- ];
-
- $cursorPosition = -1;
- $cursorInParentheses = false;
- $cursorInAttribute = false;
- $cursorInAttributeName = false;
- $cursorInPattern = false;
-
- $parenthesesBusy = false;
- $parenthesesLeft = null;
- $parenthesesRight = null;
-
- while (true) {
- $cursorPosition++;
-
- if (!isset($path[$cursorPosition])) {
- break;
- }
-
- $char = $path[$cursorPosition];
-
- if ('(' === $char && !$cursorInAttribute) {
- if ($cursorInParentheses) {
- throw new InvalidPathException(
- sprintf('[%s:%d] parentheses inside parentheses are not allowed.', $path, $cursorPosition)
- );
- }
-
- $cursorInParentheses = true;
-
- continue;
- }
-
- if ('{' === $char && !$cursorInPattern) {
- if ($cursorInAttribute) {
- throw new InvalidPathException(
- sprintf('[%s:%d] braces inside attributes are not allowed.', $path, $cursorPosition)
- );
- }
-
- if ($parenthesesBusy) {
- throw new InvalidPathException(
- sprintf('[%s:%d] multiple attributes inside parentheses are not allowed.', $path, $cursorPosition)
- );
- }
-
- if ($cursorInParentheses) {
- $parenthesesBusy = true;
- }
-
- $cursorInAttribute = true;
- $cursorInAttributeName = true;
-
- $attributeIndex++;
- $attributes[$attributeIndex] = $attributePrototype;
- $attributes[$attributeIndex]['raw'] = $char;
- $attributes[$attributeIndex]['isOptional'] = $cursorInParentheses;
- $attributes[$attributeIndex]['startPosition'] = $cursorPosition;
-
- continue;
- }
-
- if ('<' === $char && $cursorInAttribute) {
- if ($cursorInPattern) {
- throw new InvalidPathException(
- sprintf('[%s:%d] the char "<" inside patterns is not allowed.', $path, $cursorPosition)
- );
- }
-
- $cursorInPattern = true;
- $cursorInAttributeName = false;
-
- $attributes[$attributeIndex]['raw'] .= $char;
-
- continue;
- }
-
- if ('>' === $char && $cursorInAttribute) {
- if (!$cursorInPattern) {
- throw new InvalidPathException(
- sprintf('[%s:%d] at position %2$d an extra char ">" was found.', $path, $cursorPosition)
- );
- }
-
- if (null === $attributes[$attributeIndex]['pattern']) {
- throw new InvalidPathException(
- sprintf('[%s:%d] an attribute pattern is empty.', $path, $cursorPosition)
- );
- }
-
- if (isset(Router::$patterns[$attributes[$attributeIndex]['pattern']])) {
- $attributes[$attributeIndex]['pattern'] = Router::$patterns[$attributes[$attributeIndex]['pattern']];
- }
-
- $cursorInPattern = false;
-
- $attributes[$attributeIndex]['raw'] .= $char;
-
- continue;
- }
-
- if ('}' === $char && !$cursorInPattern) {
- if (!$cursorInAttribute) {
- throw new InvalidPathException(
- sprintf('[%s:%d] at position %2$d an extra closing brace was found.', $path, $cursorPosition)
- );
- }
-
- if (null === $attributes[$attributeIndex]['name']) {
- throw new InvalidPathException(
- sprintf('[%s:%d] an attribute name is empty.', $path, $cursorPosition)
- );
- }
-
- $cursorInAttribute = false;
- $cursorInAttributeName = false;
-
- $attributes[$attributeIndex]['raw'] .= $char;
- $attributes[$attributeIndex]['endPosition'] = $cursorPosition;
-
- continue;
- }
-
- if (')' === $char && !$cursorInAttribute) {
- if (!$cursorInParentheses) {
- throw new InvalidPathException(
- sprintf('[%s:%d] at position %2$d an extra closing parenthesis was found.', $path, $cursorPosition)
- );
- }
-
- if ($parenthesesBusy) {
- $attributes[$attributeIndex]['withParentheses'] = '(' . $parenthesesLeft;
- $attributes[$attributeIndex]['withParentheses'] .= $attributes[$attributeIndex]['raw'];
- $attributes[$attributeIndex]['withParentheses'] .= $parenthesesRight . ')';
- }
-
- $cursorInParentheses = false;
- $parenthesesBusy = false;
- $parenthesesLeft = null;
- $parenthesesRight = null;
-
- continue;
- }
-
- if ($cursorInParentheses && !$cursorInAttribute && !$parenthesesBusy) {
- $parenthesesLeft .= $char;
- }
-
- if ($cursorInParentheses && !$cursorInAttribute && $parenthesesBusy) {
- $parenthesesRight .= $char;
- }
-
- if ($cursorInAttribute) {
- $attributes[$attributeIndex]['raw'] .= $char;
- }
-
- if ($cursorInAttributeName) {
- if (null === $attributes[$attributeIndex]['name']) {
- if (!isset(CHARACTER_TABLE_FOR_FIRST_CHARACTER_OF_SUBPATTERN_NAME[$char])) {
- throw new InvalidPathException(
- sprintf('[%s:%d] an attribute name must begin with "A-Za-z_".', $path, $cursorPosition)
- );
- }
- }
-
- if (null !== $attributes[$attributeIndex]['name']) {
- if (!isset(CHARACTER_TABLE_FOR_SUBPATTERN_NAME[$char])) {
- throw new InvalidPathException(
- sprintf('[%s:%d] an attribute name must contain only "0-9A-Za-z_".', $path, $cursorPosition)
- );
- }
- }
-
- $attributes[$attributeIndex]['name'] .= $char;
- }
-
- if ($cursorInPattern) {
- if ('#' === $char) {
- throw new InvalidPathException(
- sprintf('[%s:%d] unallowed character "#" in an attribute pattern.', $path, $cursorPosition)
- );
- }
-
- $attributes[$attributeIndex]['pattern'] .= $char;
- }
- }
-
- if ($cursorInParentheses) {
- throw new InvalidPathException(
- sprintf('[%s] the route path contains non-closed parentheses.', $path)
- );
- }
-
- if ($cursorInAttribute) {
- throw new InvalidPathException(
- sprintf('[%s] the route path contains non-closed attribute.', $path)
- );
- }
-
- $cache[$path] = $attributes;
-
- return $attributes;
-}
diff --git a/functions/path_plain.php b/functions/path_plain.php
deleted file mode 100644
index d8638eec..00000000
--- a/functions/path_plain.php
+++ /dev/null
@@ -1,35 +0,0 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
- * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
- * @link https://github.com/sunrise-php/http-router
- */
-
-namespace Sunrise\Http\Router;
-
-/**
- * Import functions
- */
-use function str_replace;
-
-/**
- * Simplifies the given path
- *
- * @param string $path
- *
- * @return string
- */
-function path_plain(string $path) : string
-{
- $attrs = path_parse($path);
-
- foreach ($attrs as $attr) {
- $path = str_replace($attr['raw'], '{' . $attr['name'] . '}', $path);
- }
-
- return str_replace(['(', ')'], '', $path);
-}
diff --git a/functions/path_regex.php b/functions/path_regex.php
deleted file mode 100644
index 40162180..00000000
--- a/functions/path_regex.php
+++ /dev/null
@@ -1,56 +0,0 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
- * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
- * @link https://github.com/sunrise-php/http-router
- */
-
-namespace Sunrise\Http\Router;
-
-/**
- * Import functions
- */
-use function addcslashes;
-use function str_replace;
-
-/**
- * Converts the given path to Regular Expression
- *
- * @param string $path
- *
- * @return string
- */
-function path_regex(string $path) : string
-{
- // This will be useful for a long-running application,
- // for example if you use the RoadRunner server...
- static $cache = [];
-
- if (isset($cache[$path])) {
- return $cache[$path];
- }
-
- $matches = path_parse($path);
-
- foreach ($matches as $match) {
- $path = str_replace($match['raw'], '{' . $match['name'] . '}', $path);
- }
-
- $path = addcslashes($path, '#$*+-.?[\]^|');
- $path = str_replace(['(', ')'], ['(?:', ')?'], $path);
-
- foreach ($matches as $match) {
- $pattern = $match['pattern'] ?? '[^/]+';
- $subpattern = '(?<' . $match['name'] . '>' . $pattern . ')';
-
- $path = str_replace('{' . $match['name'] . '}', $subpattern, $path);
- }
-
- $cache[$path] = '#^' . $path . '$#uD';
-
- return $cache[$path];
-}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 862c7d44..ae1272cf 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -1,9 +1,12 @@
-
+
+
+
+
+ tests/*
+
- benchmarks
- functions
src
tests
diff --git a/psalm.xml.dist b/psalm.xml.dist
index 3becd9ba..43a0a144 100644
--- a/psalm.xml.dist
+++ b/psalm.xml.dist
@@ -1,9 +1,10 @@
@@ -11,4 +12,9 @@
+
+
+
+
+
diff --git a/resources/definitions/router.php b/resources/definitions/router.php
new file mode 100644
index 00000000..9806e95d
--- /dev/null
+++ b/resources/definitions/router.php
@@ -0,0 +1,80 @@
+ [],
+ 'router.middlewares' => [],
+ 'router.route_middlewares' => [],
+ 'router.parameter_resolvers' => [],
+ 'router.response_resolvers' => [],
+ 'router.event_dispatcher' => null,
+
+ ParameterResolverChainInterface::class => create(ParameterResolverChain::class)
+ ->constructor(
+ get('router.parameter_resolvers'),
+ ),
+
+ ResponseResolverChainInterface::class => create(ResponseResolverChain::class)
+ ->constructor(
+ get('router.response_resolvers'),
+ ),
+
+ ClassResolverInterface::class => create(ClassResolver::class)
+ ->constructor(
+ get(ParameterResolverChainInterface::class),
+ get(ContainerInterface::class),
+ ),
+
+ MiddlewareResolverInterface::class => create(MiddlewareResolver::class)
+ ->constructor(
+ get(ClassResolverInterface::class),
+ get(ParameterResolverChainInterface::class),
+ get(ResponseResolverChainInterface::class),
+ ),
+
+ RequestHandlerResolverInterface::class => create(RequestHandlerResolver::class)
+ ->constructor(
+ get(ClassResolverInterface::class),
+ get(ParameterResolverChainInterface::class),
+ get(ResponseResolverChainInterface::class),
+ ),
+
+ ReferenceResolverInterface::class => create(ReferenceResolver::class)
+ ->constructor(
+ get(MiddlewareResolverInterface::class),
+ get(RequestHandlerResolverInterface::class),
+ ),
+
+ RequestHandlerReflectorInterface::class => create(RequestHandlerReflector::class),
+
+ RouterInterface::class => create(Router::class)
+ ->constructor(
+ loaders: get('router.loaders'),
+ middlewares: get('router.middlewares'),
+ routeMiddlewares: get('router.route_middlewares'),
+ referenceResolver: get(ReferenceResolverInterface::class),
+ eventDispatcher: get('router.event_dispatcher'),
+ ),
+];
diff --git a/resources/translations/ru.php b/resources/translations/ru.php
new file mode 100644
index 00000000..48997d72
--- /dev/null
+++ b/resources/translations/ru.php
@@ -0,0 +1,25 @@
+ 'URI запроса невалиден и не может быть принят сервером.',
+ ErrorMessage::RESOURCE_NOT_FOUND => 'Запрашиваемый ресурс не найден для данного URI.',
+ ErrorMessage::METHOD_NOT_ALLOWED => 'Запрашиваемый метод не разрешен для данного ресурса; Проверьте заголовок ответа "Allow" на разрешенные методы.',
+ ErrorMessage::MISSING_CONTENT_TYPE => 'Заголовок запроса Content-Type должен быть предоставлен и не может быть пустым; Проверьте заголовок ответа "Accept" на поддерживаемые типы медиа.',
+ ErrorMessage::UNSUPPORTED_MEDIA_TYPE => 'Тип медиа {{ media_type }} не поддерживается; Проверьте заголовок ответа "Accept" на поддерживаемые типы медиа.',
+ ErrorMessage::INVALID_VARIABLE => 'Значение переменной {{{ variable_name }}} в URI запроса {{ route_uri }} невалидно.',
+ ErrorMessage::INVALID_QUERY => 'Параметры запроса невалидны.',
+ ErrorMessage::MISSING_HEADER => 'Заголовок запроса {{ header_name }} должен быть предоставлен.',
+ ErrorMessage::INVALID_HEADER => 'Заголовок запроса {{ header_name }} невалиден.',
+ ErrorMessage::MISSING_COOKIE => 'Отсутствует cookie {{ cookie_name }}.',
+ ErrorMessage::INVALID_COOKIE => 'Cookie {{ cookie_name }} невалидно.',
+ ErrorMessage::INVALID_BODY => 'Тело запроса невалидно.',
+ ErrorMessage::EMPTY_JSON_PAYLOAD => 'JSON payload не может быть пустым.',
+ ErrorMessage::INVALID_JSON_PAYLOAD => 'JSON payload невалидно.',
+ ErrorMessage::INVALID_JSON_PAYLOAD_FORMAT => 'JSON payload должен быть в формате массива или объекта.',
+ // phpcs:enable Generic.Files.LineLength.TooLong
+];
diff --git a/resources/translations/sr.php b/resources/translations/sr.php
new file mode 100644
index 00000000..468e8fb8
--- /dev/null
+++ b/resources/translations/sr.php
@@ -0,0 +1,25 @@
+ 'URI zahtev je nevažeći i ne može biti prihvaćen od strane servera.',
+ ErrorMessage::RESOURCE_NOT_FOUND => 'Zatraženi resurs nije pronađen za ovaj URI.',
+ ErrorMessage::METHOD_NOT_ALLOWED => 'Zatražena metoda nije dozvoljena za ovaj resurs; Proverite zaglavlje odgovora "Allow" za dozvoljene metode.',
+ ErrorMessage::MISSING_CONTENT_TYPE => 'Zaglavlje zahteva Content-Type mora biti navedeno i ne može biti prazno; Proverite zaglavlje odgovora "Accept" za podržane tipove medija.',
+ ErrorMessage::UNSUPPORTED_MEDIA_TYPE => 'Tip medija {{ media_type }} nije podržan; Proverite zaglavlje odgovora "Accept" za podržane tipove medija.',
+ ErrorMessage::INVALID_VARIABLE => 'Vrednost promenljive {{{ variable_name }}} u URI zahtevu {{ route_uri }} nije validna.',
+ ErrorMessage::INVALID_QUERY => 'Parametri upita zahteva nisu validni.',
+ ErrorMessage::MISSING_HEADER => 'Zaglavlje zahteva {{ header_name }} mora biti navedeno.',
+ ErrorMessage::INVALID_HEADER => 'Zaglavlje zahteva {{ header_name }} nije validno.',
+ ErrorMessage::MISSING_COOKIE => 'Kolačić {{ cookie_name }} nedostaje.',
+ ErrorMessage::INVALID_COOKIE => 'Kolačić {{ cookie_name }} nije validan.',
+ ErrorMessage::INVALID_BODY => 'Telo zahteva nije validno.',
+ ErrorMessage::EMPTY_JSON_PAYLOAD => 'JSON sadržaj ne može biti prazan.',
+ ErrorMessage::INVALID_JSON_PAYLOAD => 'JSON sadržaj nije validan.',
+ ErrorMessage::INVALID_JSON_PAYLOAD_FORMAT => 'JSON sadržaj mora biti u formatu niza ili objekta.',
+ // phpcs:enable Generic.Files.LineLength.TooLong
+];
diff --git a/src/Annotation/ApiOperation.php b/src/Annotation/ApiOperation.php
new file mode 100644
index 00000000..1585b19d
--- /dev/null
+++ b/src/Annotation/ApiOperation.php
@@ -0,0 +1,41 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+class ApiOperation extends Route
+{
+ /**
+ * @param string|array $method
+ */
+ public function __construct(
+ string $name,
+ string $path = '',
+ string|array $method = [],
+ array $fields = [],
+ ) {
+ parent::__construct(
+ name: $name,
+ path: $path,
+ methods: (array) $method,
+ isApiOperation: true,
+ apiOperationFields: $fields,
+ );
+ }
+}
diff --git a/src/Annotation/Constraint.php b/src/Annotation/Constraint.php
new file mode 100644
index 00000000..2b17777c
--- /dev/null
+++ b/src/Annotation/Constraint.php
@@ -0,0 +1,34 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+// phpcs:ignore Generic.Files.LineLength.TooLong
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_PARAMETER | Attribute::IS_REPEATABLE)]
+final class Constraint
+{
+ /**
+ * @var array
+ */
+ public readonly array $values;
+
+ public function __construct(mixed ...$values)
+ {
+ $this->values = $values;
+ }
+}
diff --git a/src/Annotation/Consumes.php b/src/Annotation/Consumes.php
new file mode 100644
index 00000000..159088ef
--- /dev/null
+++ b/src/Annotation/Consumes.php
@@ -0,0 +1,34 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Entity\MediaType\MediaTypeInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+class Consumes
+{
+ /**
+ * @var array
+ */
+ public readonly array $values;
+
+ public function __construct(MediaTypeInterface ...$values)
+ {
+ $this->values = $values;
+ }
+}
diff --git a/src/Annotation/ConsumesJson.php b/src/Annotation/ConsumesJson.php
new file mode 100644
index 00000000..3d289ae9
--- /dev/null
+++ b/src/Annotation/ConsumesJson.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Dictionary\MediaType;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class ConsumesJson extends Consumes
+{
+ public function __construct()
+ {
+ parent::__construct(MediaType::JSON);
+ }
+}
diff --git a/src/Annotation/ConsumesXml.php b/src/Annotation/ConsumesXml.php
new file mode 100644
index 00000000..e09458f9
--- /dev/null
+++ b/src/Annotation/ConsumesXml.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Dictionary\MediaType;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class ConsumesXml extends Consumes
+{
+ public function __construct()
+ {
+ parent::__construct(MediaType::XML);
+ }
+}
diff --git a/src/Annotation/DefaultAttribute.php b/src/Annotation/DefaultAttribute.php
new file mode 100644
index 00000000..b55c0fe1
--- /dev/null
+++ b/src/Annotation/DefaultAttribute.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class DefaultAttribute
+{
+ public function __construct(
+ public readonly string $name,
+ public readonly mixed $value,
+ ) {
+ }
+}
diff --git a/src/Annotation/DeleteApiOperation.php b/src/Annotation/DeleteApiOperation.php
new file mode 100644
index 00000000..e9be79c5
--- /dev/null
+++ b/src/Annotation/DeleteApiOperation.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class DeleteApiOperation extends ApiOperation
+{
+ public function __construct(string $name, string $path = '', array $fields = [])
+ {
+ parent::__construct($name, $path, self::METHOD_DELETE, $fields);
+ }
+}
diff --git a/src/Annotation/DeleteMethod.php b/src/Annotation/DeleteMethod.php
new file mode 100644
index 00000000..d9918cc2
--- /dev/null
+++ b/src/Annotation/DeleteMethod.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Fig\Http\Message\RequestMethodInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class DeleteMethod extends Method
+{
+ public function __construct()
+ {
+ parent::__construct(RequestMethodInterface::METHOD_DELETE);
+ }
+}
diff --git a/src/Annotation/Deprecated.php b/src/Annotation/Deprecated.php
new file mode 100644
index 00000000..0510e025
--- /dev/null
+++ b/src/Annotation/Deprecated.php
@@ -0,0 +1,24 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class Deprecated
+{
+}
diff --git a/src/Annotation/Description.php b/src/Annotation/Description.php
new file mode 100644
index 00000000..57f60491
--- /dev/null
+++ b/src/Annotation/Description.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class Description
+{
+ public function __construct(
+ public readonly string $value,
+ ) {
+ }
+}
diff --git a/src/Annotation/EmptyResponse.php b/src/Annotation/EmptyResponse.php
new file mode 100644
index 00000000..20fed114
--- /dev/null
+++ b/src/Annotation/EmptyResponse.php
@@ -0,0 +1,24 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
+final class EmptyResponse
+{
+}
diff --git a/src/Annotation/GetApiOperation.php b/src/Annotation/GetApiOperation.php
new file mode 100644
index 00000000..968f235f
--- /dev/null
+++ b/src/Annotation/GetApiOperation.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class GetApiOperation extends ApiOperation
+{
+ public function __construct(string $name, string $path = '', array $fields = [])
+ {
+ parent::__construct($name, $path, self::METHOD_GET, $fields);
+ }
+}
diff --git a/src/Annotation/GetMethod.php b/src/Annotation/GetMethod.php
new file mode 100644
index 00000000..c8c8754c
--- /dev/null
+++ b/src/Annotation/GetMethod.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Fig\Http\Message\RequestMethodInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class GetMethod extends Method
+{
+ public function __construct()
+ {
+ parent::__construct(RequestMethodInterface::METHOD_GET);
+ }
+}
diff --git a/src/Annotation/HeadApiOperation.php b/src/Annotation/HeadApiOperation.php
new file mode 100644
index 00000000..c7182a5e
--- /dev/null
+++ b/src/Annotation/HeadApiOperation.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class HeadApiOperation extends ApiOperation
+{
+ public function __construct(string $name, string $path = '', array $fields = [])
+ {
+ parent::__construct($name, $path, self::METHOD_HEAD, $fields);
+ }
+}
diff --git a/src/Annotation/HeadMethod.php b/src/Annotation/HeadMethod.php
new file mode 100644
index 00000000..4ab35b09
--- /dev/null
+++ b/src/Annotation/HeadMethod.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Fig\Http\Message\RequestMethodInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class HeadMethod extends Method
+{
+ public function __construct()
+ {
+ parent::__construct(RequestMethodInterface::METHOD_HEAD);
+ }
+}
diff --git a/src/Annotation/Host.php b/src/Annotation/Host.php
deleted file mode 100644
index 209c4f5c..00000000
--- a/src/Annotation/Host.php
+++ /dev/null
@@ -1,52 +0,0 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
- * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
- * @link https://github.com/sunrise-php/http-router
- */
-
-namespace Sunrise\Http\Router\Annotation;
-
-/**
- * Import classes
- */
-use Attribute;
-
-/**
- * @Annotation
- *
- * @Target({"CLASS", "METHOD"})
- *
- * @NamedArgumentConstructor
- *
- * @Attributes({
- * @Attribute("value", type="string", required=true),
- * })
- *
- * @since 2.11.0
- */
-#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD)]
-final class Host
-{
-
- /**
- * The attribute value
- *
- * @var string
- */
- public $value;
-
- /**
- * Constructor of the class
- *
- * @param string $value
- */
- public function __construct(string $value)
- {
- $this->value = $value;
- }
-}
diff --git a/src/Annotation/JsonResponse.php b/src/Annotation/JsonResponse.php
new file mode 100644
index 00000000..d8cbcd7a
--- /dev/null
+++ b/src/Annotation/JsonResponse.php
@@ -0,0 +1,38 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Dictionary\MediaType;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
+final class JsonResponse extends SerializableResponse
+{
+ public function __construct(
+ public readonly ?int $encodingFlags = null,
+ public readonly ?int $encodingDepth = null,
+ ) {
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getMediaTypes(): array
+ {
+ return [MediaType::JSON];
+ }
+}
diff --git a/src/Annotation/Method.php b/src/Annotation/Method.php
new file mode 100644
index 00000000..e328197b
--- /dev/null
+++ b/src/Annotation/Method.php
@@ -0,0 +1,36 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Fig\Http\Message\RequestMethodInterface;
+
+/**
+ * Pay attention to the {@see RequestMethodInterface} dictionary.
+ *
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+class Method implements RequestMethodInterface
+{
+ /**
+ * @var array
+ */
+ public readonly array $values;
+
+ public function __construct(string ...$values)
+ {
+ $this->values = $values;
+ }
+}
diff --git a/src/Annotation/Middleware.php b/src/Annotation/Middleware.php
index eba445c1..b63d2e6d 100644
--- a/src/Annotation/Middleware.php
+++ b/src/Annotation/Middleware.php
@@ -1,52 +1,33 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
+ * @author Anatoly Nekhay
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
* @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
* @link https://github.com/sunrise-php/http-router
*/
+declare(strict_types=1);
+
namespace Sunrise\Http\Router\Annotation;
-/**
- * Import classes
- */
use Attribute;
/**
- * @Annotation
- *
- * @Target({"CLASS", "METHOD"})
- *
- * @NamedArgumentConstructor
- *
- * @Attributes({
- * @Attribute("value", type="string", required=true),
- * })
- *
* @since 2.11.0
*/
-#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)]
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class Middleware
{
-
/**
- * The attribute value
- *
- * @var string
+ * @var array
*/
- public $value;
+ public readonly array $values;
- /**
- * Constructor of the class
- *
- * @param string $value
- */
- public function __construct(string $value)
+ public function __construct(mixed ...$values)
{
- $this->value = $value;
+ $this->values = $values;
}
}
diff --git a/src/Annotation/NamePrefix.php b/src/Annotation/NamePrefix.php
new file mode 100644
index 00000000..1614babb
--- /dev/null
+++ b/src/Annotation/NamePrefix.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class NamePrefix
+{
+ public function __construct(
+ public readonly string $value,
+ ) {
+ }
+}
diff --git a/src/Annotation/OptionsApiOperation.php b/src/Annotation/OptionsApiOperation.php
new file mode 100644
index 00000000..3387888c
--- /dev/null
+++ b/src/Annotation/OptionsApiOperation.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class OptionsApiOperation extends ApiOperation
+{
+ public function __construct(string $name, string $path = '', array $fields = [])
+ {
+ parent::__construct($name, $path, self::METHOD_OPTIONS, $fields);
+ }
+}
diff --git a/src/Annotation/OptionsMethod.php b/src/Annotation/OptionsMethod.php
new file mode 100644
index 00000000..a9cc090a
--- /dev/null
+++ b/src/Annotation/OptionsMethod.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Fig\Http\Message\RequestMethodInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class OptionsMethod extends Method
+{
+ public function __construct()
+ {
+ parent::__construct(RequestMethodInterface::METHOD_OPTIONS);
+ }
+}
diff --git a/src/Annotation/PatchApiOperation.php b/src/Annotation/PatchApiOperation.php
new file mode 100644
index 00000000..259211aa
--- /dev/null
+++ b/src/Annotation/PatchApiOperation.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class PatchApiOperation extends ApiOperation
+{
+ public function __construct(string $name, string $path = '', array $fields = [])
+ {
+ parent::__construct($name, $path, self::METHOD_PATCH, $fields);
+ }
+}
diff --git a/src/Annotation/PatchMethod.php b/src/Annotation/PatchMethod.php
new file mode 100644
index 00000000..23858358
--- /dev/null
+++ b/src/Annotation/PatchMethod.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Fig\Http\Message\RequestMethodInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class PatchMethod extends Method
+{
+ public function __construct()
+ {
+ parent::__construct(RequestMethodInterface::METHOD_PATCH);
+ }
+}
diff --git a/src/Annotation/Path.php b/src/Annotation/Path.php
new file mode 100644
index 00000000..a9e6f7bd
--- /dev/null
+++ b/src/Annotation/Path.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class Path
+{
+ public function __construct(
+ public readonly string $value,
+ ) {
+ }
+}
diff --git a/src/Annotation/PathPostfix.php b/src/Annotation/PathPostfix.php
new file mode 100644
index 00000000..8b276933
--- /dev/null
+++ b/src/Annotation/PathPostfix.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 2.11.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class PathPostfix
+{
+ public function __construct(
+ public readonly string $value,
+ ) {
+ }
+}
diff --git a/src/Annotation/PathPrefix.php b/src/Annotation/PathPrefix.php
new file mode 100644
index 00000000..abe65754
--- /dev/null
+++ b/src/Annotation/PathPrefix.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 2.11.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class PathPrefix
+{
+ public function __construct(
+ public readonly string $value,
+ ) {
+ }
+}
diff --git a/src/Annotation/Pattern.php b/src/Annotation/Pattern.php
new file mode 100644
index 00000000..1f15f19d
--- /dev/null
+++ b/src/Annotation/Pattern.php
@@ -0,0 +1,32 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Dictionary\VariablePattern;
+
+/**
+ * Pay attention to the {@see VariablePattern} dictionary.
+ *
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+class Pattern
+{
+ public function __construct(
+ public readonly string $variableName,
+ public readonly string $value,
+ ) {
+ }
+}
diff --git a/src/Annotation/PostApiOperation.php b/src/Annotation/PostApiOperation.php
new file mode 100644
index 00000000..4ae3fc64
--- /dev/null
+++ b/src/Annotation/PostApiOperation.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class PostApiOperation extends ApiOperation
+{
+ public function __construct(string $name, string $path = '', array $fields = [])
+ {
+ parent::__construct($name, $path, self::METHOD_POST, $fields);
+ }
+}
diff --git a/src/Annotation/PostMethod.php b/src/Annotation/PostMethod.php
new file mode 100644
index 00000000..ad439e12
--- /dev/null
+++ b/src/Annotation/PostMethod.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Fig\Http\Message\RequestMethodInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class PostMethod extends Method
+{
+ public function __construct()
+ {
+ parent::__construct(RequestMethodInterface::METHOD_POST);
+ }
+}
diff --git a/src/Annotation/Postfix.php b/src/Annotation/Postfix.php
deleted file mode 100644
index 53f7ab82..00000000
--- a/src/Annotation/Postfix.php
+++ /dev/null
@@ -1,52 +0,0 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
- * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
- * @link https://github.com/sunrise-php/http-router
- */
-
-namespace Sunrise\Http\Router\Annotation;
-
-/**
- * Import classes
- */
-use Attribute;
-
-/**
- * @Annotation
- *
- * @Target({"CLASS", "METHOD"})
- *
- * @NamedArgumentConstructor
- *
- * @Attributes({
- * @Attribute("value", type="string", required=true),
- * })
- *
- * @since 2.11.0
- */
-#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD)]
-final class Postfix
-{
-
- /**
- * The attribute value
- *
- * @var string
- */
- public $value;
-
- /**
- * Constructor of the class
- *
- * @param string $value
- */
- public function __construct(string $value)
- {
- $this->value = $value;
- }
-}
diff --git a/src/Annotation/Prefix.php b/src/Annotation/Prefix.php
deleted file mode 100644
index 84fdfc5c..00000000
--- a/src/Annotation/Prefix.php
+++ /dev/null
@@ -1,52 +0,0 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
- * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
- * @link https://github.com/sunrise-php/http-router
- */
-
-namespace Sunrise\Http\Router\Annotation;
-
-/**
- * Import classes
- */
-use Attribute;
-
-/**
- * @Annotation
- *
- * @Target({"CLASS", "METHOD"})
- *
- * @NamedArgumentConstructor
- *
- * @Attributes({
- * @Attribute("value", type="string", required=true),
- * })
- *
- * @since 2.11.0
- */
-#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD)]
-final class Prefix
-{
-
- /**
- * The attribute value
- *
- * @var string
- */
- public $value;
-
- /**
- * Constructor of the class
- *
- * @param string $value
- */
- public function __construct(string $value)
- {
- $this->value = $value;
- }
-}
diff --git a/src/Annotation/Priority.php b/src/Annotation/Priority.php
new file mode 100644
index 00000000..690db471
--- /dev/null
+++ b/src/Annotation/Priority.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class Priority
+{
+ public function __construct(
+ public readonly int $value,
+ ) {
+ }
+}
diff --git a/src/Annotation/Produces.php b/src/Annotation/Produces.php
new file mode 100644
index 00000000..b911b801
--- /dev/null
+++ b/src/Annotation/Produces.php
@@ -0,0 +1,34 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Entity\MediaType\MediaTypeInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+class Produces
+{
+ /**
+ * @var array
+ */
+ public readonly array $values;
+
+ public function __construct(MediaTypeInterface ...$values)
+ {
+ $this->values = $values;
+ }
+}
diff --git a/src/Annotation/ProducesJson.php b/src/Annotation/ProducesJson.php
new file mode 100644
index 00000000..324413bf
--- /dev/null
+++ b/src/Annotation/ProducesJson.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Dictionary\MediaType;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class ProducesJson extends Produces
+{
+ public function __construct()
+ {
+ parent::__construct(MediaType::JSON);
+ }
+}
diff --git a/src/Annotation/ProducesXml.php b/src/Annotation/ProducesXml.php
new file mode 100644
index 00000000..deca49dd
--- /dev/null
+++ b/src/Annotation/ProducesXml.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Dictionary\MediaType;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class ProducesXml extends Produces
+{
+ public function __construct()
+ {
+ parent::__construct(MediaType::XML);
+ }
+}
diff --git a/src/Annotation/PurgeApiOperation.php b/src/Annotation/PurgeApiOperation.php
new file mode 100644
index 00000000..e1c63b6b
--- /dev/null
+++ b/src/Annotation/PurgeApiOperation.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class PurgeApiOperation extends ApiOperation
+{
+ public function __construct(string $name, string $path = '', array $fields = [])
+ {
+ parent::__construct($name, $path, self::METHOD_PURGE, $fields);
+ }
+}
diff --git a/src/Annotation/PurgeMethod.php b/src/Annotation/PurgeMethod.php
new file mode 100644
index 00000000..59cf127b
--- /dev/null
+++ b/src/Annotation/PurgeMethod.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Fig\Http\Message\RequestMethodInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class PurgeMethod extends Method
+{
+ public function __construct()
+ {
+ parent::__construct(RequestMethodInterface::METHOD_PURGE);
+ }
+}
diff --git a/src/Annotation/PutApiOperation.php b/src/Annotation/PutApiOperation.php
new file mode 100644
index 00000000..5e0ef048
--- /dev/null
+++ b/src/Annotation/PutApiOperation.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+final class PutApiOperation extends ApiOperation
+{
+ public function __construct(string $name, string $path = '', array $fields = [])
+ {
+ parent::__construct($name, $path, self::METHOD_PUT, $fields);
+ }
+}
diff --git a/src/Annotation/PutMethod.php b/src/Annotation/PutMethod.php
new file mode 100644
index 00000000..abb1a392
--- /dev/null
+++ b/src/Annotation/PutMethod.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Fig\Http\Message\RequestMethodInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class PutMethod extends Method
+{
+ public function __construct()
+ {
+ parent::__construct(RequestMethodInterface::METHOD_PUT);
+ }
+}
diff --git a/src/Annotation/RequestBody.php b/src/Annotation/RequestBody.php
new file mode 100644
index 00000000..f1266408
--- /dev/null
+++ b/src/Annotation/RequestBody.php
@@ -0,0 +1,30 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_PARAMETER)]
+final class RequestBody
+{
+ public function __construct(
+ public readonly ?int $errorStatusCode = null,
+ public readonly ?string $errorMessage = null,
+ public readonly ?bool $validationEnabled = null,
+ ) {
+ }
+}
diff --git a/src/Annotation/RequestCookie.php b/src/Annotation/RequestCookie.php
new file mode 100644
index 00000000..c3cba4aa
--- /dev/null
+++ b/src/Annotation/RequestCookie.php
@@ -0,0 +1,31 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_PARAMETER)]
+final class RequestCookie
+{
+ public function __construct(
+ public readonly string $name,
+ public readonly ?int $errorStatusCode = null,
+ public readonly ?string $errorMessage = null,
+ public readonly ?bool $validationEnabled = null,
+ ) {
+ }
+}
diff --git a/src/Annotation/RequestHeader.php b/src/Annotation/RequestHeader.php
new file mode 100644
index 00000000..1fcb6df1
--- /dev/null
+++ b/src/Annotation/RequestHeader.php
@@ -0,0 +1,31 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_PARAMETER)]
+final class RequestHeader
+{
+ public function __construct(
+ public readonly string $name,
+ public readonly ?int $errorStatusCode = null,
+ public readonly ?string $errorMessage = null,
+ public readonly ?bool $validationEnabled = null,
+ ) {
+ }
+}
diff --git a/src/Annotation/RequestQuery.php b/src/Annotation/RequestQuery.php
new file mode 100644
index 00000000..e9995486
--- /dev/null
+++ b/src/Annotation/RequestQuery.php
@@ -0,0 +1,30 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_PARAMETER)]
+final class RequestQuery
+{
+ public function __construct(
+ public readonly ?int $errorStatusCode = null,
+ public readonly ?string $errorMessage = null,
+ public readonly ?bool $validationEnabled = null,
+ ) {
+ }
+}
diff --git a/src/Annotation/RequestVariable.php b/src/Annotation/RequestVariable.php
new file mode 100644
index 00000000..0310a90c
--- /dev/null
+++ b/src/Annotation/RequestVariable.php
@@ -0,0 +1,31 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_PARAMETER)]
+final class RequestVariable
+{
+ public function __construct(
+ public readonly ?string $name = null,
+ public readonly ?int $errorStatusCode = null,
+ public readonly ?string $errorMessage = null,
+ public readonly ?bool $validationEnabled = null,
+ ) {
+ }
+}
diff --git a/src/Annotation/ResponseHeader.php b/src/Annotation/ResponseHeader.php
new file mode 100644
index 00000000..24f4b4b4
--- /dev/null
+++ b/src/Annotation/ResponseHeader.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)]
+final class ResponseHeader
+{
+ public function __construct(
+ public readonly string $name,
+ public readonly string $value,
+ ) {
+ }
+}
diff --git a/src/Annotation/ResponseStatus.php b/src/Annotation/ResponseStatus.php
new file mode 100644
index 00000000..00d3cbf8
--- /dev/null
+++ b/src/Annotation/ResponseStatus.php
@@ -0,0 +1,32 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Fig\Http\Message\StatusCodeInterface;
+
+/**
+ * Pay attention to the {@see StatusCodeInterface} dictionary.
+ *
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
+final class ResponseStatus implements StatusCodeInterface
+{
+ public function __construct(
+ public readonly int $code,
+ public readonly string $phrase = '',
+ ) {
+ }
+}
diff --git a/src/Annotation/Route.php b/src/Annotation/Route.php
index 1b7d5c7b..114818af 100644
--- a/src/Annotation/Route.php
+++ b/src/Annotation/Route.php
@@ -1,172 +1,63 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
+ * @author Anatoly Nekhay
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
* @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
* @link https://github.com/sunrise-php/http-router
*/
+declare(strict_types=1);
+
namespace Sunrise\Http\Router\Annotation;
-/**
- * Import classes
- */
use Attribute;
+use Fig\Http\Message\RequestMethodInterface;
+use Sunrise\Http\Router\Entity\MediaType\MediaTypeInterface;
/**
- * @Annotation
- *
- * @Target({"CLASS", "METHOD"})
- *
- * @NamedArgumentConstructor
- *
- * @Attributes({
- * @Attribute("name", type="string", required=true),
- * @Attribute("host", type="string"),
- * @Attribute("path", type="string", required=true),
- * @Attribute("method", type="string"),
- * @Attribute("methods", type="array"),
- * @Attribute("middlewares", type="array"),
- * @Attribute("attributes", type="array"),
- * @Attribute("summary", type="string"),
- * @Attribute("description", type="string"),
- * @Attribute("tags", type="array"),
- * @Attribute("priority", type="integer"),
- * })
+ * @since 2.0.0
*/
-#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD)]
-final class Route
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+class Route implements RequestMethodInterface
{
+ public mixed $holder = null;
- /**
- * The descriptor holder
- *
- * @var mixed
- *
- * @internal
- */
- public $holder;
-
- /**
- * A route name
- *
- * @var string
- */
- public $name;
-
- /**
- * A route host
- *
- * @var string|null
- */
- public $host;
+ /** @var array */
+ public array $namePrefixes = [];
- /**
- * A route path
- *
- * @var string
- */
- public $path;
+ /** @var array */
+ public array $pathPrefixes = [];
- /**
- * A route methods
- *
- * @var string[]
- */
- public $methods;
+ /** @var non-empty-string|null */
+ public ?string $pattern = null;
- /**
- * A route middlewares
- *
- * @var string[]
- */
- public $middlewares;
-
- /**
- * A route attributes
- *
- * @var array
- */
- public $attributes;
-
- /**
- * A route summary
- *
- * @var string
- */
- public $summary;
-
- /**
- * A route description
- *
- * @var string
- */
- public $description;
-
- /**
- * A route tags
- *
- * @var string[]
- */
- public $tags;
-
- /**
- * A route priority
- *
- * @var int
- */
- public $priority;
-
- /**
- * Constructor of the class
- *
- * @param string $name The route name
- * @param string|null $host The route host
- * @param string $path The route path
- * @param string|null $method The route method
- * @param string[] $methods The route methods
- * @param string[] $middlewares The route middlewares
- * @param array $attributes The route attributes
- * @param string $summary The route summary
- * @param string $description The route description
- * @param string[] $tags The route tags
- * @param int $priority The route priority (default 0)
- */
public function __construct(
- string $name,
- ?string $host = null,
- string $path = '/',
- ?string $method = null,
- array $methods = [],
- array $middlewares = [],
- array $attributes = [],
- string $summary = '',
- string $description = '',
- array $tags = [],
- int $priority = 0
+ public string $name,
+ public string $path = '',
+ /** @var array */
+ public array $patterns = [],
+ /** @var array */
+ public array $methods = [],
+ /** @var array */
+ public array $attributes = [],
+ /** @var array */
+ public array $middlewares = [],
+ /** @var array */
+ public array $consumes = [],
+ /** @var array */
+ public array $produces = [],
+ /** @var array */
+ public array $tags = [],
+ public string $summary = '',
+ public string $description = '',
+ public bool $isDeprecated = false,
+ public bool $isApiOperation = false,
+ /** @var array|object|null */
+ public array|object|null $apiOperationFields = null,
+ public int $priority = 0,
) {
- if (isset($method)) {
- $methods[] = $method;
- }
-
- // if no methods are specified,
- // such a route is a GET route.
- if (empty($methods)) {
- $methods[] = 'GET';
- }
-
- $this->name = $name;
- $this->host = $host;
- $this->path = $path;
- $this->methods = $methods;
- $this->middlewares = $middlewares;
- $this->attributes = $attributes;
- $this->summary = $summary;
- $this->description = $description;
- $this->tags = $tags;
- $this->priority = $priority;
}
}
diff --git a/src/Annotation/SerializableResponse.php b/src/Annotation/SerializableResponse.php
new file mode 100644
index 00000000..1ef67ef5
--- /dev/null
+++ b/src/Annotation/SerializableResponse.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Entity\MediaType\MediaTypeInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
+abstract class SerializableResponse
+{
+ /**
+ * @return array
+ */
+ abstract public function getMediaTypes(): array;
+}
diff --git a/src/Annotation/SlugPattern.php b/src/Annotation/SlugPattern.php
new file mode 100644
index 00000000..2138a54a
--- /dev/null
+++ b/src/Annotation/SlugPattern.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Dictionary\VariablePattern;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class SlugPattern extends Pattern
+{
+ public function __construct(string $variableName)
+ {
+ parent::__construct($variableName, VariablePattern::SLUG);
+ }
+}
diff --git a/src/Annotation/Summary.php b/src/Annotation/Summary.php
new file mode 100644
index 00000000..9d0a3398
--- /dev/null
+++ b/src/Annotation/Summary.php
@@ -0,0 +1,28 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class Summary
+{
+ public function __construct(
+ public readonly string $value,
+ ) {
+ }
+}
diff --git a/src/Annotation/Tag.php b/src/Annotation/Tag.php
new file mode 100644
index 00000000..55de6f94
--- /dev/null
+++ b/src/Annotation/Tag.php
@@ -0,0 +1,33 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class Tag
+{
+ /**
+ * @var array
+ */
+ public readonly array $values;
+
+ public function __construct(string ...$values)
+ {
+ $this->values = $values;
+ }
+}
diff --git a/src/Annotation/UintPattern.php b/src/Annotation/UintPattern.php
new file mode 100644
index 00000000..c503aa48
--- /dev/null
+++ b/src/Annotation/UintPattern.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Dictionary\VariablePattern;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class UintPattern extends Pattern
+{
+ public function __construct(string $variableName)
+ {
+ parent::__construct($variableName, VariablePattern::UINT);
+ }
+}
diff --git a/src/Annotation/UuidPattern.php b/src/Annotation/UuidPattern.php
new file mode 100644
index 00000000..bd7b486a
--- /dev/null
+++ b/src/Annotation/UuidPattern.php
@@ -0,0 +1,29 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Annotation;
+
+use Attribute;
+use Sunrise\Http\Router\Dictionary\VariablePattern;
+
+/**
+ * @since 3.0.0
+ */
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class UuidPattern extends Pattern
+{
+ public function __construct(string $variableName)
+ {
+ parent::__construct($variableName, VariablePattern::UUID);
+ }
+}
diff --git a/src/ClassResolver.php b/src/ClassResolver.php
new file mode 100644
index 00000000..3faf690a
--- /dev/null
+++ b/src/ClassResolver.php
@@ -0,0 +1,85 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router;
+
+use InvalidArgumentException;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\ContainerInterface;
+use ReflectionClass;
+use ReflectionException;
+
+use function class_exists;
+use function sprintf;
+
+/**
+ * @since 3.0.0
+ */
+final class ClassResolver implements ClassResolverInterface
+{
+ /**
+ * @var array
+ */
+ private array $resolvedClasses = [];
+
+ public function __construct(
+ private readonly ParameterResolverChainInterface $parameterResolverChain,
+ private readonly ?ContainerInterface $container,
+ ) {
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param class-string $className
+ *
+ * @return T
+ *
+ * @template T of object
+ *
+ * @throws ContainerExceptionInterface
+ * @throws InvalidArgumentException
+ * @throws ReflectionException
+ */
+ public function resolveClass(string $className): object
+ {
+ if ($this->container?->has($className) === true) {
+ /** @var T */
+ return $this->container->get($className);
+ }
+
+ if (isset($this->resolvedClasses[$className])) {
+ /** @var T */
+ return $this->resolvedClasses[$className];
+ }
+
+ if (!class_exists($className)) {
+ throw new InvalidArgumentException(sprintf('The class %s does not exist.', $className));
+ }
+
+ /** @var ReflectionClass $classReflection */
+ $classReflection = new ReflectionClass($className);
+
+ if (!$classReflection->isInstantiable()) {
+ throw new InvalidArgumentException(sprintf('The class %s is not instantiable.', $className));
+ }
+
+ $this->resolvedClasses[$className] = $classReflection->newInstance(
+ ...$this->parameterResolverChain->resolveParameters(
+ ...($classReflection->getConstructor()?->getParameters() ?? [])
+ )
+ );
+
+ return $this->resolvedClasses[$className];
+ }
+}
diff --git a/src/ClassResolverInterface.php b/src/ClassResolverInterface.php
new file mode 100644
index 00000000..68a0ec5c
--- /dev/null
+++ b/src/ClassResolverInterface.php
@@ -0,0 +1,35 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router;
+
+use InvalidArgumentException;
+
+/**
+ * @since 3.0.0
+ */
+interface ClassResolverInterface
+{
+ /**
+ * Tries to resolve the given named class
+ *
+ * @param class-string $className
+ *
+ * @return T
+ *
+ * @template T of object
+ *
+ * @throws InvalidArgumentException
+ */
+ public function resolveClass(string $className): object;
+}
diff --git a/src/Command/RouteListCommand.php b/src/Command/RouteListCommand.php
deleted file mode 100644
index 15c7aff3..00000000
--- a/src/Command/RouteListCommand.php
+++ /dev/null
@@ -1,128 +0,0 @@
-
- * @copyright Copyright (c) 2018, Anatoly Fenric
- * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
- * @link https://github.com/sunrise-php/http-router
- */
-
-namespace Sunrise\Http\Router\Command;
-
-/**
- * Import classes
- */
-use RuntimeException;
-use Sunrise\Http\Router\Router;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Helper\Table;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-
-/**
- * Import functions
- */
-use function join;
-use function sprintf;
-use function Sunrise\Http\Router\path_plain;
-
-/**
- * This command will list all routes in your application
- *
- * If you cannot pass the router to the constructor
- * or your architecture has problems with autowiring,
- * then just inherit this class and override the getRouter method.
- *
- * @since 2.9.0
- */
-class RouteListCommand extends Command
-{
-
- /**
- * {@inheritdoc}
- */
- protected static $defaultName = 'router:route-list';
-
- /**
- * {@inheritdoc}
- *
- * @var string
- */
- protected static $defaultDescription = 'Lists all routes in your application';
-
- /**
- * The router instance populated with routes
- *
- * @var Router|null
- */
- private $router;
-
- /**
- * Constructor of the class
- *
- * @param Router|null $router
- */
- public function __construct(?Router $router = null)
- {
- parent::__construct();
-
- $this->setName(static::$defaultName);
- $this->setDescription(static::$defaultDescription);
-
- $this->router = $router;
- }
-
- /**
- * Gets the router instance populated with routes
- *
- * @return Router
- *
- * @throws RuntimeException
- * If the command doesn't contain the router instance.
- *
- * @since 2.11.0
- */
- protected function getRouter() : Router
- {
- if (null === $this->router) {
- throw new RuntimeException(sprintf(
- 'The %2$s() method MUST return the %1$s class instance. ' .
- 'Pass the %1$s class instance to the constructor, or override the %2$s() method.',
- Router::class,
- __METHOD__
- ));
- }
-
- return $this->router;
- }
-
- /**
- * {@inheritdoc}
- */
- final protected function execute(InputInterface $input, OutputInterface $output) : int
- {
- $table = new Table($output);
-
- $table->setHeaders([
- 'Name',
- 'Host',
- 'Path',
- 'Verb',
- ]);
-
- foreach ($this->getRouter()->getRoutes() as $route) {
- $table->addRow([
- $route->getName(),
- $route->getHost() ?? 'ANY',
- path_plain($route->getPath()),
- join(', ', $route->getMethods()),
- ]);
- }
-
- $table->render();
-
- return 0;
- }
-}
diff --git a/src/Command/RouterClearDescriptorsCacheCommand.php b/src/Command/RouterClearDescriptorsCacheCommand.php
new file mode 100644
index 00000000..420ef01b
--- /dev/null
+++ b/src/Command/RouterClearDescriptorsCacheCommand.php
@@ -0,0 +1,45 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Command;
+
+use Sunrise\Http\Router\Loader\DescriptorLoaderInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * @since 3.0.0
+ */
+#[AsCommand('router:clear-descriptors-cache', 'Clears the descriptors cache.')]
+final class RouterClearDescriptorsCacheCommand extends Command
+{
+ public function __construct(
+ private readonly DescriptorLoaderInterface $descriptorLoader,
+ ) {
+ parent::__construct();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->descriptorLoader->clearCache();
+
+ $output->writeln('Done');
+
+ return self::SUCCESS;
+ }
+}
diff --git a/src/Command/RouterGenerateOpenApiDocumentCommand.php b/src/Command/RouterGenerateOpenApiDocumentCommand.php
new file mode 100644
index 00000000..4da82793
--- /dev/null
+++ b/src/Command/RouterGenerateOpenApiDocumentCommand.php
@@ -0,0 +1,508 @@
+
+ * @copyright Copyright (c) 2018, Anatoly Nekhay
+ * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
+ * @link https://github.com/sunrise-php/http-router
+ */
+
+declare(strict_types=1);
+
+namespace Sunrise\Http\Router\Command;
+
+use ArrayAccess;
+use BackedEnum;
+use DateTimeImmutable;
+use DateTimeInterface;
+use DateTimeZone;
+use ReflectionAttribute;
+use ReflectionClass;
+use ReflectionEnum;
+use ReflectionException;
+use ReflectionMethod;
+use ReflectionNamedType;
+use ReflectionParameter;
+use ReflectionProperty;
+use ReflectionType;
+use Sunrise\Http\Message\Response;
+use Sunrise\Http\Router\Annotation\RequestBody;
+use Sunrise\Http\Router\Annotation\RequestCookie;
+use Sunrise\Http\Router\Annotation\RequestHeader;
+use Sunrise\Http\Router\Annotation\ResponseStatus;
+use Sunrise\Http\Router\Annotation\SerializableResponse;
+use Sunrise\Http\Router\Helper\RouteBuilder;
+use Sunrise\Http\Router\Helper\RouteParser;
+use Sunrise\Http\Router\Helper\RouteSimplifier;
+use Sunrise\Http\Router\RequestHandlerReflectorInterface;
+use Sunrise\Http\Router\RouteInterface;
+use Sunrise\Http\Router\RouterInterface;
+use Sunrise\Hydrator\Annotation\Alias;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+use function array_merge;
+use function class_exists;
+use function compact;
+use function date;
+use function end;
+use function in_array;
+use function is_a;
+use function is_subclass_of;
+use function json_encode;
+use function sprintf;
+use function strtolower;
+
+use function strtr;
+use const JSON_PRETTY_PRINT;
+use const JSON_UNESCAPED_SLASHES;
+use const JSON_UNESCAPED_UNICODE;
+use const PHP_INT_SIZE;
+
+/**
+ * @since 3.0.0
+ */
+#[AsCommand('router:generate-open-api-document', 'Generates OpenAPI Document.')]
+final class RouterGenerateOpenApiDocumentCommand extends Command
+{
+ private const BUILT_IN_TYPE_BOOL = 'bool';
+ private const BUILT_IN_TYPE_INT = 'int';
+ private const BUILT_IN_TYPE_FLOAT = 'float';
+ private const BUILT_IN_TYPE_STRING = 'string';
+ private const BUILT_IN_TYPE_ARRAY = 'array';
+
+ private static array $schemas = [];
+
+ public function __construct(
+ private readonly RouterInterface $router,
+ private readonly RequestHandlerReflectorInterface $requestHandlerReflector,
+ private readonly array $document,
+ private readonly string $dateTimeFormat,
+ ) {
+ parent::__construct();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $document = $this->document;
+
+ foreach ($this->router->getRoutes() as $route) {
+ if (!$route->isApiOperation()) {
+ continue;
+ }
+
+ $controller = $this->requestHandlerReflector->reflectRequestHandler($route->getRequestHandler());
+ if (! $controller instanceof ReflectionMethod) {
+ continue;
+ }
+
+ self::enrichDocumentWithPaths($route, $controller, $document);
+ }
+
+ foreach (self::$schemas as $name => $schema) {
+ $document['components']['schemas'][$name] = $schema;
+ }
+
+ /** @var string $json */
+ $json = json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+
+ $output->writeln($json);
+
+ return self::SUCCESS;
+ }
+
+ private static function enrichDocumentWithPaths(
+ RouteInterface $route,
+ ReflectionMethod $controller,
+ array &$document,
+ ): void {
+ $operation = $route->getApiOperationFields();
+ $operation['operationId'] = $route->getName();
+ $operation['tags'] = $route->getTags();
+ $operation['summary'] = $route->getSummary();
+ $operation['description'] = $route->getDescription();
+ $operation['deprecated'] = $route->isDeprecated();
+
+ self::enrichOperationWithPathParameters($route, $operation);
+ self::enrichOperationWithCookieAndHeaderParameters($controller, $operation);
+ self::enrichOperationWithRequestBody($route, $controller, $operation);
+ self::enrichOperationWithSerializableResponse($route, $controller, $operation);
+
+ $routePath = RouteSimplifier::simplifyRoute($route->getPath());
+ foreach ($route->getMethods() as $routeMethod) {
+ $routeMethod = strtolower($routeMethod);
+ $document['paths'][$routePath][$routeMethod] = $operation;
+ }
+ }
+
+ private static function enrichOperationWithPathParameters(RouteInterface $route, array &$operation): void
+ {
+ $routeVars = RouteParser::parseRoute($route->getPath());
+ $routePatterns = $route->getPatterns();
+ $routeAttributes = $route->getAttributes();
+
+ foreach ($routeVars as $routeVar) {
+ $parameter = [];
+ $parameter['in'] = 'path';
+ $parameter['name'] = $routeVar['name'];
+ $parameter['schema']['type'] = 'string';
+
+ $pattern = $routeVar['pattern'] ?? $routePatterns[$routeVar['name']] ?? null;
+ if ($pattern !== null) {
+ $parameter['schema']['pattern'] = '^' . $pattern . '$';
+ }
+
+ if (isset($routeAttributes[$routeVar['name']])) {
+ $default = RouteBuilder::stringifyValue($routeAttributes[$routeVar['name']]);
+ $parameter['schema']['default'] = $default;
+ }
+
+ if (!isset($routeVar['optional_part'])) {
+ $parameter['required'] = true;
+ }
+
+ $operation['parameters'][] = $parameter;
+ }
+ }
+
+ private static function enrichOperationWithCookieAndHeaderParameters(
+ ReflectionMethod $endpoint,
+ array &$operation,
+ ): void {
+ foreach ($endpoint->getParameters() as $endpointParameter) {
+ /** @var list> $annotations */
+ $annotations = array_merge(
+ $endpointParameter->getAttributes(RequestCookie::class),
+ $endpointParameter->getAttributes(RequestHeader::class),
+ );
+
+ foreach ($annotations as $annotation) {
+ $annotation = $annotation->newInstance();
+
+ $operationParameter = [];
+
+ $operationParameter['in'] = match ($annotation::class) {
+ RequestCookie::class => 'cookie',
+ RequestHeader::class => 'header',
+ };
+
+ $operationParameter['name'] = $annotation->name;
+ $operationParameter['schema'] = self::getStringSchema();
+
+ if (!$endpointParameter->isDefaultValueAvailable()) {
+ $operationParameter['required'] = true;
+ }
+
+ $operation['parameters'][] = $operationParameter;
+ }
+ }
+ }
+
+ private static function enrichOperationWithRequestBody(
+ RouteInterface $route,
+ ReflectionMethod $controller,
+ array &$operation,
+ ): void {
+ foreach ($controller->getParameters() as $parameter) {
+ if ($parameter->getAttributes(RequestBody::class) === []) {
+ continue;
+ }
+
+ $schema = self::getTypeSchema($parameter->getType());
+
+ foreach ($route->getConsumedMediaTypes() as $mediaType) {
+ $operation['requestBody']['content'][$mediaType->getIdentifier()]['schema'] = $schema;
+ }
+
+ return;
+ }
+ }
+
+ private static function enrichOperationWithSerializableResponse(
+ RouteInterface $route,
+ ReflectionMethod $controller,
+ array &$operation,
+ ): void {
+ if ($controller->getAttributes(SerializableResponse::class, ReflectionAttribute::IS_INSTANCEOF) === []) {
+ return;
+ }
+
+ $schema = self::getTypeSchema($controller->getReturnType());
+
+ $responseStatusCode = 200;
+ $responseStatusPhrase = 'Operation completed successfully.';
+
+ /** @var list> $annotations */
+ $annotations = $controller->getAttributes(ResponseStatus::class);
+ if (isset($annotations[0])) {
+ $responseStatus = $annotations[0]->newInstance();
+ $responseStatusCode = $responseStatus->code;
+ }
+
+ foreach ($route->getProducedMediaTypes() as $mediaType) {
+ $operation['responses'][$responseStatusCode]['content'][$mediaType->getIdentifier()]['schema'] = $schema;
+ $operation['responses'][$responseStatusCode]['description'] = $responseStatusPhrase;
+ }
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ private static function getTypeSchema(?ReflectionType $type): array
+ {
+ if ($type === null) {
+ return [];
+ }
+
+ $schema = [];
+
+ $typeName = (string) $type;
+
+ if ($typeName === self::BUILT_IN_TYPE_BOOL) {
+ $schema = self::getBoolSchema();
+ } elseif ($typeName === self::BUILT_IN_TYPE_INT) {
+ $schema = self::getIntSchema();
+ } elseif ($typeName === self::BUILT_IN_TYPE_FLOAT) {
+ $schema = self::getFloatSchema();
+ } elseif ($typeName === self::BUILT_IN_TYPE_STRING) {
+ $schema = self::getStringSchema();
+ } elseif ($typeName === self::BUILT_IN_TYPE_ARRAY) {
+ $schema = self::getArraySchema();
+ } elseif (is_subclass_of($typeName, BackedEnum::class)) {
+ $schema = self::getEnumSchema($typeName, nullable: $type->allowsNull());
+ } elseif (is_subclass_of($typeName, ArrayAccess::class)) {
+ $schema = self::getCollectionSchema($typeName);
+ } elseif (is_a($typeName, DateTimeImmutable::class, true)) {
+ $schema = self::getStringSchema();
+ $schema['format'] = 'date-time';
+ } elseif (class_exists($typeName)) {
+ $schema = self::getClassSchema($typeName);
+ }
+
+ if ($type->allowsNull()) {
+ $schema['nullable'] = true;
+ }
+
+ return $schema;
+ }
+
+ private static function getBoolSchema(): array
+ {
+ return [
+ 'type' => 'boolean',
+ ];
+ }
+
+ private static function getIntSchema(): array
+ {
+ return [
+ 'type' => 'integer',
+ 'format' => PHP_INT_SIZE === 4 ? 'int32' : 'int64',
+ ];
+ }
+
+ private static function getFloatSchema(): array
+ {
+ return [
+ 'type' => 'number',
+ 'format' => 'double',
+ ];
+ }
+
+ private static function getStringSchema(): array
+ {
+ return [
+ 'type' => 'string',
+ ];
+ }
+
+ /**
+ * @return array{oneOf: array{0: array{type: 'array'}, 1: array{type: 'object'}}}
+ */
+ private static function getArraySchema(): array
+ {
+ return [
+ 'oneOf' => [
+ [
+ 'type' => 'array',
+ ],
+ [
+ 'type' => 'object',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @param class-string $enumName
+ *
+ * @throws ReflectionException
+ */
+ private static function getEnumSchema(string $enumName, bool $nullable = false): array
+ {
+ $schemaName = strtr($enumName, '\\', '.');
+ $schemaRef = self::getSchemaRef($schemaName);
+
+ if (isset(self::$schemas[$schemaName])) {
+ return $schemaRef;
+ }
+
+ $schema = match ((string) (new ReflectionEnum($enumName))->getBackingType()) {
+ self::BUILT_IN_TYPE_INT => self::getIntSchema(),
+ self::BUILT_IN_TYPE_STRING => self::getStringSchema(),
+ };
+
+ $schema['enum'] = [];
+ foreach ($enumName::cases() as $case) {
+ $schema['enum'][] = $case->value;
+ }
+
+ // https://swagger.io/docs/specification/data-models/enums/#nullable
+ if ($nullable) {
+ $schema['enum'][] = null;
+ }
+
+ self::$schemas[$schemaName] = $schema;
+
+ return $schemaRef;
+ }
+
+ /**
+ * @link https://github.com/sunrise-php/hydrator/blob/main/README.md#array
+ *
+ * @param class-string $className
+ *
+ * @throws ReflectionException
+ */
+ private static function getCollectionSchema(string $className): array
+ {
+ $schemaName = strtr($className, '\\', '.');
+ $schemaRef = self::getSchemaRef($schemaName);
+
+ if (isset(self::$schemas[$schemaName])) {
+ return $schemaRef;
+ }
+
+ $class = new ReflectionClass($className);
+
+ $schema = self::getArraySchema();
+
+ $constructor = $class->getConstructor();
+ if ($constructor === null) {
+ return $schema;
+ }
+
+ $parameters = $constructor->getParameters();
+ if ($parameters === []) {
+ return $schema;
+ }
+
+ /** @var ReflectionParameter $parameter */
+ $parameter = end($parameters);
+ if (!$parameter->isVariadic()) {
+ return $schema;
+ }
+
+ $type = $parameter->getType();
+ if ($type === null) {
+ return $schema;
+ }
+
+ $elementSchema = self::getTypeSchema($type);
+
+ $schema['oneOf'][0]['items'] = $elementSchema;
+ $schema['oneOf'][1]['additionalProperties'] = $elementSchema;
+
+ self::$schemas[$schemaName] = $schema;
+
+ return $schemaRef;
+ }
+
+ /**
+ * @param class-string