├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── phpcs.xml ├── phpstan.neon ├── phpunit.xml └── src ├── Cache ├── FileCache.php └── Router.php ├── ContainerAwareInterface.php ├── ContainerAwareTrait.php ├── Dispatcher.php ├── Http ├── Exception.php ├── Exception │ ├── BadRequestException.php │ ├── ConflictException.php │ ├── ExpectationFailedException.php │ ├── ForbiddenException.php │ ├── GoneException.php │ ├── HttpExceptionInterface.php │ ├── ImATeapotException.php │ ├── LengthRequiredException.php │ ├── MethodNotAllowedException.php │ ├── NotAcceptableException.php │ ├── NotFoundException.php │ ├── PreconditionFailedException.php │ ├── PreconditionRequiredException.php │ ├── TooManyRequestsException.php │ ├── UnauthorizedException.php │ ├── UnavailableForLegalReasonsException.php │ ├── UnprocessableEntityException.php │ └── UnsupportedMediaException.php ├── Request.php └── Response │ └── Decorator │ └── DefaultHeaderDecorator.php ├── Middleware ├── MiddlewareAwareInterface.php └── MiddlewareAwareTrait.php ├── Route.php ├── RouteCollectionInterface.php ├── RouteCollectionTrait.php ├── RouteConditionHandlerInterface.php ├── RouteConditionHandlerTrait.php ├── RouteGroup.php ├── Router.php └── Strategy ├── AbstractStrategy.php ├── ApplicationStrategy.php ├── JsonStrategy.php ├── OptionsHandlerInterface.php ├── StrategyAwareInterface.php ├── StrategyAwareTrait.php └── StrategyInterface.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 120 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [philipobenito] 4 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: ~ 5 | pull_request: ~ 6 | 7 | jobs: 8 | phpcs: 9 | name: PHPCS 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: 8.4 18 | extensions: curl, mbstring 19 | coverage: none 20 | tools: composer:v2, cs2pr 21 | 22 | - run: composer update --no-progress 23 | - run: vendor/bin/phpcs -q --report=checkstyle | cs2pr 24 | 25 | phpunit: 26 | name: PHPUnit on ${{ matrix.php }} ${{ matrix.composer-flags }} 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | php: ['8.1', '8.2', '8.3', '8.4'] 31 | coverage: [true] 32 | composer-flags: [''] 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | with: 37 | fetch-depth: 0 38 | 39 | - uses: shivammathur/setup-php@v2 40 | with: 41 | php-version: ${{ matrix.php }} 42 | extensions: curl, mbstring 43 | coverage: pcov 44 | tools: composer:v2 45 | 46 | # - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 47 | 48 | - run: composer update --no-progress ${{ matrix.composer-flags }} 49 | 50 | - run: vendor/bin/phpunit --no-coverage 51 | if: ${{ !matrix.coverage }} 52 | 53 | - run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover 54 | if: ${{ matrix.coverage }} 55 | 56 | - run: php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover 57 | if: ${{ matrix.coverage }} 58 | 59 | phpstan: 60 | name: PHPStan 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - uses: actions/checkout@v2 65 | 66 | - uses: shivammathur/setup-php@v2 67 | with: 68 | php-version: 8.4 69 | extensions: curl, mbstring 70 | coverage: none 71 | tools: composer:v2 72 | 73 | - run: composer update --no-progress 74 | - run: vendor/bin/phpstan analyse --no-progress 75 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [6.2.0] 2024-11 8 | 9 | ### Changed 10 | - Replaced opis/closure with laravel/serializable-closure and implemented throughout the handler process rather than a blanket serialisation of the router. 11 | 12 | ## [6.1.1] 2024-11 13 | 14 | ### Fixed 15 | - Further fixes for type hinting bug related to array based callables with a string class name. 16 | 17 | ## [6.1.0] 2024-11 18 | 19 | ### Fixed 20 | - Fixed a bug introduced in 6.0.0 where an array based callable with a string class name would not be considered valid. 21 | - Added some doc comments for clarity on array types. (@marekskopal) 22 | 23 | ### Changed 24 | - Updated `psr/http-message` to `^2.0.0`. 25 | 26 | ## [6.0.0] 2024-11 27 | 28 | > Note: While this is a major release, there are no breaking changes to the public API. The major version bump is due to the removal of support for PHP 8.0 and below. 29 | > 30 | > This being said, there are some internal changes that may affect you if you have extended the library in any way. Please test thoroughly before upgrading. 31 | 32 | ### Added 33 | - Added full support for PHP 8.1 to 8.4. 34 | - Ability to use a PSR-15 middleware as a controller. 35 | - Ability to pass an array of HTTP methods to `Router::map` to create a route that matches multiple methods. 36 | - This method still accepts a string so is not a breaking change. 37 | - Ability to add a custom key to a caching router. 38 | 39 | ### Changed 40 | - Fixes and improvements throughout for PHP 8.1 to 8.4. 41 | 42 | ### Removed 43 | - Removed support for PHP < 8.1. 44 | 45 | ## [5.1.0] 2021-07 46 | 47 | ### Added 48 | - Support for named routes within groups (@Fredrik82) 49 | 50 | ## [5.0.1] 2021-03 51 | 52 | ### Added 53 | - Support for `psr/container:2.0` 54 | 55 | ## [5.0.0] 2021-01 56 | 57 | ### Added 58 | - A cached router, a way to have a fully built router cached and resolved from cache on subsequent requests. 59 | - Response decorators, a way to manipulate a response object returned from a matched route. 60 | - Automatic generation of OPTIONS routes if they have not been defined. 61 | 62 | ### Changed 63 | - Minimum PHP requirement bumped to 7.2. 64 | - `Router` no longer extends FastRoute `RouteCollecter`. 65 | - `Router` constructor no longer accepts optional FastRoute `RouteParser` and `DataGenerator`. 66 | - `Router` constructor now accepts an optional FastRoute `RouteCollector`. 67 | - Routes already registered with FastRoute `RouteCollector` are respected and matched. 68 | - Separated route preparation from dispatch process so that the router can dispatch multiple times. 69 | - General code improvements. 70 | 71 | ### Removed 72 | - Setting of default response headers on strategies. (Replaced by response decorators, see Added). 73 | - Exception handlers from strategies. (Already deprecated in favour of throwable handlers). 74 | 75 | ## [4.5.1] 2021.01 76 | 77 | ### Added 78 | - Official support for PHP 8.0. 79 | 80 | ## [4.5.0] 2020-05 81 | 82 | ### Added 83 | - Ability to pass optional `$replacements` array to `Route::getPath` in order to build literal route path strings. 84 | 85 | ## [4.4.0] 2020-05 86 | 87 | ### Added 88 | - Ability to pass JSON flags to JsonStrategy. (@pine3ree) 89 | - Router is now a RequestHandlerInterface so can be used as a middleware itself. (@delboy1978uk) 90 | - Route params now added as Request attributes. (@delboy1978uk) 91 | 92 | ### Fixed 93 | - Exception moved to more appropriate place when shifting no middleware. (@delboy1978uk) 94 | - Ensure group prefix is always added when adding a parent group. (@delboy1978uk) 95 | 96 | 97 | ## [4.3.1] 2019-07 98 | 99 | ### Fixed 100 | - Fixed bug when attempting to get a container for custom strategy that is not container aware. 101 | 102 | ## [4.3.0] 2019-06 103 | 104 | ### Added 105 | - Ability to add middleware to the stack as a class name so it is only instantiated when used. 106 | 107 | ### Changed 108 | - Switch to use `zendframework/zend-httphandlerrunner` as removed from `diactoros` (@JohnstonCode) 109 | 110 | ### Fixed 111 | - When adding a prefix to a group after adding routes, it is now applied to those routes. (@delboy1978uk) 112 | - Fix to how shifting middleware is handled to prevent error triggering. (@delboy1978uk) 113 | - Fix to ensure that when invoking FastRoute methods on League\Route all callables are converted to League\Route objects (@pgk) 114 | - Various documentation fixes. 115 | 116 | ## [4.2.0] 2018-10 117 | 118 | ### Added 119 | - Allow adding default response headers to strategies. 120 | - Expand error handling to include Throwable. 121 | 122 | ## [4.1.1] 2018-10 123 | 124 | ### Fixed 125 | - Fixed issue where group middleware was being dublicated on internal routes. 126 | 127 | ## [4.1.0] 2018-09 128 | 129 | ### Changed 130 | - JSON strategy now allows array and object returns and builds JSON response. (Thanks @willemwollebrants) 131 | 132 | ### Fixed 133 | - Fixed issue where setting strategy on specific routes had no effect. (Thanks @aag) 134 | 135 | ## [4.0.1] 2018-08 136 | 137 | ### Fixed 138 | - Fixed a bug where content-type header would not be added to response in Json Strategy. 139 | 140 | ## [4.0.0] 2018-08 141 | 142 | ### Changed 143 | - Increased minimum PHP version to 7.1.0 144 | - Now implements PSR-15 middleware and request handlers. 145 | - No longer enforces use of container, one can be used optionally. 146 | - Strategies now return PSR-15 middleare as handlers. 147 | - Increased types of proxy callables that can be used as controllers. 148 | - General housekeeping and refactoring for continued improvement. 149 | 150 | ### Fixed 151 | - Group level strategies now handle exceptions if a route is not matched but the request falls within the group. 152 | 153 | ## [3.1.0] 2018-07 154 | 155 | ### Fixed 156 | - Ensure JsonStrategy handles all exceptions by default. 157 | - Handle multiline exception messages. 158 | 159 | ### Added 160 | - Add port condition to routes. 161 | 162 | ## 3.0.4 2017-03 163 | 164 | ### Fixed 165 | - Middleware execution order. 166 | 167 | ## 3.0.0 2017-03 168 | 169 | ## Added 170 | - Middleware functionality for PSR-7 compatible callables, globally to route collection or individually per route/group. 171 | - Allow setting of strategy for a route group. 172 | - Add UUID as default pattern matcher. 173 | 174 | ## Changed 175 | - Now depend directly on PSR-11 implementation. 176 | - Simplified default strategies to just `Application` and `Json`. 177 | - Have strategies return a middleware to add to the stack. 178 | - Have strategies handle decoration of exceptions. 179 | 180 | ## 2.0.2 - 2018-07 181 | 182 | ### Fixed 183 | - Have JsonStrategy handle all exceptions by default. 184 | 185 | ## 2.0.0 - 2016-02 186 | 187 | ### Added 188 | - All routing and dispatching now built around PSR-7. 189 | - Can now group routes with prefix and match conditions. 190 | - Routes now stored against a specific `Route` object that describes the route. 191 | - New `dispatch` method on `RouteCollection` that is a compliant PSR-7 middleware. 192 | - Additional route matching conditions for scheme and host. 193 | 194 | ### Changed 195 | - API rewrite to simplify. 196 | - API naming improvements. 197 | - Strategies now less opinionated about return from controller. 198 | 199 | ## [1.2.0] - 2015-08 200 | 201 | ### Added 202 | - Can now use any callable as a controller. 203 | - Request object is now built by the strategy when one is not available from the container. 204 | 205 | ### Fixed 206 | - General tidying and removal of unused code. 207 | - URI variables now correctly passed to controller in `MethodArgumentStrategy`. 208 | 209 | ## [1.1.0] - 2015-02 210 | 211 | ### Added 212 | - Added `addPatternMatcher` method to allow custom regex shortcuts within wildcard routes. 213 | - Refactored logic around matching routes. 214 | 215 | ## [1.0.1] - 2015-01 216 | 217 | ### Fixed 218 | - Added import statements for all used objects. 219 | - Fixed dockblock annotations. 220 | - PSR-2 standards improvements within tests. 221 | 222 | ## 1.0.0 - 2015-01 223 | 224 | ### Added 225 | - Migrated from [Orno\Route](https://github.com/orno/route). 226 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/thephpleague/route). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ vendor/bin/phpunit 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Phil Bennett 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Route 2 | 3 | [![Author](https://img.shields.io/badge/author-Phil%20Bennett-blue?style=flat-square)](https://github.com/philipobenito) 4 | [![Latest Version](https://img.shields.io/github/release/thephpleague/route.svg?style=flat-square)](https://github.com/thephpleague/route/releases) 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 6 | [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/thephpleague/route/test.yml?style=flat-square)](https://github.com/thephpleague/route/actions/workflows/test.yml) 7 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/thephpleague/route.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/route/code-structure) 8 | [![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/route.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/route) 9 | [![Total Downloads](https://img.shields.io/packagist/dt/league/route.svg?style=flat-square)](https://packagist.org/packages/league/route) 10 | 11 | This package is compliant with [PSR-1], [PSR-2], [PSR-4], [PSR-7], [PSR-11], [PSR-12] and [PSR-15]. If you notice compliance oversights, please send a patch via pull request. 12 | 13 | [PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md 14 | [PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md 15 | [PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md 16 | [PSR-7]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md 17 | [PSR-11]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md 18 | [PSR-12]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md 19 | [PSR-15]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-15-request-handlers.md 20 | 21 | ## Install 22 | 23 | Via Composer 24 | 25 | ``` bash 26 | $ composer require league/route 27 | ``` 28 | 29 | ## Requirements 30 | 31 | The following versions of PHP are supported by this version. 32 | 33 | * PHP 8.1 34 | * PHP 8.2 35 | * PHP 8.3 36 | * PHP 8.4 37 | 38 | ## Documentation 39 | 40 | Route has [full documentation](http://route.thephpleague.com), powered by [Jekyll](http://jekyllrb.com/). 41 | 42 | Contribute to this documentation in the [docs directory](https://github.com/thephpleague/route/tree/master/docs/). 43 | 44 | ## Testing 45 | 46 | ``` bash 47 | $ vendor/bin/phpunit 48 | ``` 49 | 50 | ## Contributing 51 | 52 | Please see [CONTRIBUTING](https://github.com/thephpleague/route/blob/master/CONTRIBUTING.md) for details. 53 | 54 | ## Credits 55 | 56 | - [Phil Bennett](https://github.com/philipobenito) 57 | - [Nikita Popov](https://github.com/nikic) ([FastRoute](https://github.com/nikic/FastRoute)) 58 | - [All Contributors](https://github.com/thephpleague/route/contributors) 59 | 60 | ## License 61 | 62 | The MIT License (MIT). Please see [License File](https://github.com/thephpleague/route/blob/master/LICENSE.md) for more information. 63 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "league/route", 3 | "description": "Fast routing and dispatch component including PSR-15 middleware, built on top of FastRoute.", 4 | "keywords": [ 5 | "league", 6 | "route", 7 | "router", 8 | "dispatcher", 9 | "psr-7", 10 | "psr7", 11 | "psr-15", 12 | "psr15" 13 | ], 14 | "homepage": "https://github.com/thephpleague/route", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Phil Bennett", 19 | "email": "mail@philbennett.co.uk", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": "^8.1", 25 | "laravel/serializable-closure": "^2.0.0", 26 | "nikic/fast-route": "^1.3", 27 | "psr/container": "^2.0", 28 | "psr/http-factory": "^1.0", 29 | "psr/http-message": "^2.0", 30 | "psr/http-server-handler": "^1.0.1", 31 | "psr/http-server-middleware": "^1.0.1", 32 | "psr/simple-cache": "^3.0" 33 | }, 34 | "require-dev": { 35 | "laminas/laminas-diactoros": "^3.5", 36 | "phpstan/phpstan": "^1.12", 37 | "phpstan/phpstan-phpunit": "^1.3", 38 | "phpunit/phpunit": "^10.2", 39 | "roave/security-advisories": "dev-latest", 40 | "scrutinizer/ocular": "^1.8", 41 | "squizlabs/php_codesniffer": "^3.7" 42 | }, 43 | "replace": { 44 | "orno/route": "~1.0", 45 | "orno/http": "~1.0" 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "League\\Route\\": "src" 50 | } 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "League\\Route\\": "tests" 55 | }, 56 | "files": ["tests/Fixture/function.php"] 57 | }, 58 | "extra": { 59 | "branch-alias": { 60 | "dev-master": "6.x-dev", 61 | "dev-6.x": "6.x-dev", 62 | "dev-5.x": "5.x-dev", 63 | "dev-4.x": "4.x-dev", 64 | "dev-3.x": "3.x-dev", 65 | "dev-2.x": "2.x-dev", 66 | "dev-1.x": "1.x-dev" 67 | } 68 | }, 69 | "scripts": { 70 | "analyse": "vendor/bin/phpstan analyse -l 4 --no-progress src", 71 | "check": "vendor/bin/phpunit && vendor/bin/phpstan analyse -l 4 --no-progress src", 72 | "test": "vendor/bin/phpunit" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | src 14 | tests 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 4 3 | paths: 4 | - src 5 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | tests 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | src/ 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Cache/FileCache.php: -------------------------------------------------------------------------------- 1 | has($key)) ? file_get_contents($this->cacheFilePath) : $default; 18 | } 19 | 20 | public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool 21 | { 22 | return (bool) file_put_contents($this->cacheFilePath, $value); 23 | } 24 | 25 | public function has(string $key): bool 26 | { 27 | return file_exists($this->cacheFilePath) && time() - filemtime($this->cacheFilePath) < $this->ttl; 28 | } 29 | 30 | public function delete(string $key): bool 31 | { 32 | return unlink($this->cacheFilePath); 33 | } 34 | 35 | public function clear(): bool 36 | { 37 | return $this->delete($this->cacheFilePath); 38 | } 39 | 40 | public function getMultiple(iterable $keys, mixed $default = null): iterable 41 | { 42 | return []; 43 | } 44 | 45 | public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool 46 | { 47 | return false; 48 | } 49 | 50 | public function deleteMultiple(iterable $keys): bool 51 | { 52 | return false; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Cache/Router.php: -------------------------------------------------------------------------------- 1 | cacheEnabled && $builder instanceof \Closure) { 35 | $builder = new SerializableClosure($builder); 36 | } 37 | 38 | $this->builder = $builder; 39 | } 40 | 41 | /** 42 | * @throws \Psr\SimpleCache\InvalidArgumentException 43 | */ 44 | public function dispatch(ServerRequestInterface $request): ResponseInterface 45 | { 46 | $router = $this->buildRouter($request); 47 | return $router->dispatch($request); 48 | } 49 | 50 | /** 51 | * @throws \Psr\SimpleCache\InvalidArgumentException 52 | */ 53 | protected function buildRouter(ServerRequestInterface $request): MainRouter 54 | { 55 | if (true === $this->cacheEnabled && $cache = $this->cache->get($this->cacheKey)) { 56 | $router = unserialize($cache, ['allowed_classes' => true]); 57 | 58 | if ($router instanceof MainRouter) { 59 | return $router; 60 | } 61 | } 62 | 63 | $builder = $this->builder; 64 | 65 | if ($builder instanceof SerializableClosure) { 66 | $builder = $builder->getClosure(); 67 | } 68 | 69 | $router = $builder(new MainRouter()); 70 | 71 | if (false === $this->cacheEnabled) { 72 | return $router; 73 | } 74 | 75 | if ($router instanceof MainRouter) { 76 | $router->prepareRoutes($request); 77 | $this->cache->set($this->cacheKey, serialize($router)); 78 | return $router; 79 | } 80 | 81 | throw new InvalidArgumentException('Invalid Router builder provided to cached router'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ContainerAwareInterface.php: -------------------------------------------------------------------------------- 1 | container; 17 | } 18 | 19 | public function setContainer(ContainerInterface $container): ContainerAwareInterface 20 | { 21 | $this->container = $container; 22 | 23 | if ($this instanceof ContainerAwareInterface) { 24 | return $this; 25 | } 26 | 27 | throw new RuntimeException(sprintf( 28 | 'Trait (%s) must be consumed by an instance of (%s)', 29 | __TRAIT__, 30 | ContainerAwareInterface::class 31 | )); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Dispatcher.php: -------------------------------------------------------------------------------- 1 | getMethod(); 29 | $uri = $request->getUri()->getPath(); 30 | $match = $this->dispatch($method, $uri); 31 | 32 | switch ($match[0]) { 33 | case FastRoute::NOT_FOUND: 34 | $this->setNotFoundDecoratorMiddleware(); 35 | break; 36 | case FastRoute::METHOD_NOT_ALLOWED: 37 | $allowed = (array) $match[1]; 38 | $this->setMethodNotAllowedDecoratorMiddleware($allowed); 39 | break; 40 | case FastRoute::FOUND: 41 | $route = $this->ensureHandlerIsRoute($match[1], $method, $uri)->setVars($match[2]); 42 | 43 | if ($this->isExtraConditionMatch($route, $request)) { 44 | $this->setFoundMiddleware($route); 45 | $request = $this->requestWithRouteAttributes($request, $route); 46 | break; 47 | } 48 | 49 | $this->setNotFoundDecoratorMiddleware(); 50 | break; 51 | } 52 | 53 | return $this->handle($request); 54 | } 55 | 56 | public function handle(ServerRequestInterface $request): ResponseInterface 57 | { 58 | $middleware = $this->shiftMiddleware(); 59 | return $middleware->process($request, $this); 60 | } 61 | 62 | protected function ensureHandlerIsRoute($matchingHandler, $httpMethod, $uri): Route 63 | { 64 | if ($matchingHandler instanceof Route) { 65 | return $matchingHandler; 66 | } 67 | 68 | return new Route($httpMethod, $uri, $matchingHandler); 69 | } 70 | 71 | protected function requestWithRouteAttributes(ServerRequestInterface $request, Route $route): ServerRequestInterface 72 | { 73 | $routerParams = $route->getVars(); 74 | 75 | foreach ($routerParams as $key => $value) { 76 | $request = $request->withAttribute($key, $value); 77 | } 78 | 79 | return $request; 80 | } 81 | 82 | protected function setFoundMiddleware(Route $route): void 83 | { 84 | if ($route->getStrategy() === null) { 85 | $strategy = $this->getStrategy(); 86 | 87 | if (!($strategy instanceof StrategyInterface)) { 88 | throw new RuntimeException('Cannot determine strategy to use for dispatch of found route'); 89 | } 90 | 91 | $route->setStrategy($strategy); 92 | } 93 | 94 | $strategy = $route->getStrategy(); 95 | $container = $strategy instanceof ContainerAwareInterface ? $strategy->getContainer() : null; 96 | 97 | foreach ($this->getMiddlewareStack() as $key => $middleware) { 98 | $this->middleware[$key] = $this->resolveMiddleware($middleware, $container); 99 | } 100 | 101 | // wrap entire dispatch process in exception handler 102 | $this->prependMiddleware($strategy->getThrowableHandler()); 103 | 104 | // add group and route specific middleware 105 | if ($group = $route->getParentGroup()) { 106 | foreach ($group->getMiddlewareStack() as $middleware) { 107 | $this->middleware($this->resolveMiddleware($middleware, $container)); 108 | } 109 | } 110 | 111 | foreach ($route->getMiddlewareStack() as $middleware) { 112 | $this->middleware($this->resolveMiddleware($middleware, $container)); 113 | } 114 | 115 | // add actual route to end of stack 116 | $this->middleware($route); 117 | } 118 | 119 | protected function setMethodNotAllowedDecoratorMiddleware(array $allowed): void 120 | { 121 | $strategy = $this->getStrategy(); 122 | 123 | if (!($strategy instanceof StrategyInterface)) { 124 | throw new RuntimeException('Cannot determine strategy to use for dispatch of method not allowed route'); 125 | } 126 | 127 | $middleware = $strategy->getMethodNotAllowedDecorator(new MethodNotAllowedException($allowed)); 128 | $this->prependMiddleware($middleware); 129 | } 130 | 131 | protected function setNotFoundDecoratorMiddleware(): void 132 | { 133 | $strategy = $this->getStrategy(); 134 | 135 | if (!($strategy instanceof StrategyInterface)) { 136 | throw new RuntimeException('Cannot determine strategy to use for dispatch of not found route'); 137 | } 138 | 139 | $middleware = $strategy->getNotFoundDecorator(new NotFoundException()); 140 | $this->prependMiddleware($middleware); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Http/Exception.php: -------------------------------------------------------------------------------- 1 | message, $code, $previous); 20 | } 21 | 22 | public function getStatusCode(): int 23 | { 24 | return $this->status; 25 | } 26 | 27 | public function getHeaders(): array 28 | { 29 | return $this->headers; 30 | } 31 | 32 | public function buildJsonResponse(ResponseInterface $response): ResponseInterface 33 | { 34 | $this->headers['content-type'] = 'application/json'; 35 | 36 | foreach ($this->headers as $key => $value) { 37 | /** @var ResponseInterface $response */ 38 | $response = $response->withAddedHeader($key, $value); 39 | } 40 | 41 | if ($response->getBody()->isWritable()) { 42 | $response->getBody()->write(json_encode([ 43 | 'status_code' => $this->status, 44 | 'reason_phrase' => $this->message 45 | ])); 46 | } 47 | 48 | return $response->withStatus($this->status, $this->message); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Http/Exception/BadRequestException.php: -------------------------------------------------------------------------------- 1 | implode(', ', $allowed) 20 | ]; 21 | 22 | parent::__construct(405, $message, $previous, $headers, $code); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Http/Exception/NotAcceptableException.php: -------------------------------------------------------------------------------- 1 | addDefaultHeaders($headers); 16 | } 17 | 18 | public function __invoke(ResponseInterface $response): ResponseInterface 19 | { 20 | foreach ($this->headers as $name => $value) { 21 | if (false === $response->hasHeader($name)) { 22 | $response = $response->withAddedHeader($name, $value); 23 | } 24 | } 25 | 26 | return $response; 27 | } 28 | 29 | public function addDefaultHeader(string $name, string $value): self 30 | { 31 | $this->headers[$name] = $value; 32 | return $this; 33 | } 34 | 35 | public function addDefaultHeaders(array $headers): self 36 | { 37 | foreach ($headers as $name => $value) { 38 | $this->addDefaultHeader($name, $value); 39 | } 40 | 41 | return $this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Middleware/MiddlewareAwareInterface.php: -------------------------------------------------------------------------------- 1 | middleware; 22 | } 23 | 24 | public function lazyMiddleware(string $middleware): MiddlewareAwareInterface 25 | { 26 | $this->middleware[] = $middleware; 27 | return $this; 28 | } 29 | 30 | public function lazyMiddlewares(array $middlewares): MiddlewareAwareInterface 31 | { 32 | foreach ($middlewares as $middleware) { 33 | $this->lazyMiddleware($middleware); 34 | } 35 | 36 | return $this; 37 | } 38 | 39 | public function lazyPrependMiddleware(string $middleware): MiddlewareAwareInterface 40 | { 41 | array_unshift($this->middleware, $middleware); 42 | return $this; 43 | } 44 | 45 | public function middleware(MiddlewareInterface $middleware): MiddlewareAwareInterface 46 | { 47 | $this->middleware[] = $middleware; 48 | return $this; 49 | } 50 | 51 | public function middlewares(array $middlewares): MiddlewareAwareInterface 52 | { 53 | foreach ($middlewares as $middleware) { 54 | $this->middleware($middleware); 55 | } 56 | 57 | return $this; 58 | } 59 | 60 | public function prependMiddleware(MiddlewareInterface $middleware): MiddlewareAwareInterface 61 | { 62 | array_unshift($this->middleware, $middleware); 63 | return $this; 64 | } 65 | 66 | public function shiftMiddleware(): MiddlewareInterface 67 | { 68 | $middleware = array_shift($this->middleware); 69 | 70 | if ($middleware === null) { 71 | throw new OutOfBoundsException('Reached end of middleware stack. Does your controller return a response?'); 72 | } 73 | 74 | return $middleware; 75 | } 76 | 77 | protected function resolveMiddleware($middleware, ?ContainerInterface $container = null): MiddlewareInterface 78 | { 79 | if ($container === null && is_string($middleware) && class_exists($middleware)) { 80 | $middleware = new $middleware(); 81 | } 82 | 83 | if ($container !== null && is_string($middleware) && $container->has($middleware)) { 84 | $middleware = $container->get($middleware); 85 | } 86 | 87 | if ($middleware instanceof MiddlewareInterface) { 88 | return $middleware; 89 | } 90 | 91 | throw new InvalidArgumentException(sprintf('Could not resolve middleware class: %s', $middleware)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Route.php: -------------------------------------------------------------------------------- 1 | |string $method 31 | * @param array $vars 32 | */ 33 | public function __construct( 34 | protected array|string $method, 35 | protected string $path, 36 | callable|array|string|RequestHandlerInterface $handler, 37 | protected ?RouteGroup $group = null, 38 | protected array $vars = [] 39 | ) { 40 | if ($handler instanceof \Closure) { 41 | $handler = new SerializableClosure($handler); 42 | } 43 | 44 | $this->handler = ($handler instanceof RequestHandlerInterface) ? [$handler, 'handle'] : $handler; 45 | } 46 | 47 | public function getCallable(?ContainerInterface $container = null): callable 48 | { 49 | $callable = $this->handler; 50 | 51 | if ($callable instanceof SerializableClosure) { 52 | $callable = $callable->getClosure(); 53 | } 54 | 55 | if (is_string($callable) && str_contains($callable, '::')) { 56 | $callable = explode('::', $callable); 57 | } 58 | 59 | if (is_array($callable) && isset($callable[0]) && is_object($callable[0])) { 60 | $callable = [$callable[0], $callable[1]]; 61 | } 62 | 63 | if (is_array($callable) && isset($callable[0]) && is_string($callable[0])) { 64 | $callable = [$this->resolve($callable[0], $container), $callable[1]]; 65 | } 66 | 67 | if (is_string($callable)) { 68 | $callable = $this->resolve($callable, $container); 69 | } 70 | 71 | if ($callable instanceof RequestHandlerInterface) { 72 | $callable = [$callable, 'handle']; 73 | } 74 | 75 | if (!is_callable($callable)) { 76 | throw new RuntimeException('Could not resolve a callable for this route'); 77 | } 78 | 79 | return $callable; 80 | } 81 | 82 | /** 83 | * @return array|string 84 | */ 85 | public function getMethod(): array|string 86 | { 87 | return $this->method; 88 | } 89 | 90 | public function getParentGroup(): ?RouteGroup 91 | { 92 | return $this->group; 93 | } 94 | 95 | public function getPath(array $replacements = []): string 96 | { 97 | $toReplace = []; 98 | 99 | foreach ($replacements as $wildcard => $actual) { 100 | $toReplace['/{' . preg_quote($wildcard, '/') . '(:.*?)?}/'] = $actual; 101 | } 102 | 103 | return preg_replace(array_keys($toReplace), array_values($toReplace), $this->path); 104 | } 105 | 106 | /** 107 | * @return array 108 | */ 109 | public function getVars(): array 110 | { 111 | return $this->vars; 112 | } 113 | 114 | public function process( 115 | ServerRequestInterface $request, 116 | RequestHandlerInterface $handler 117 | ): ResponseInterface { 118 | $strategy = $this->getStrategy(); 119 | 120 | if (!($strategy instanceof StrategyInterface)) { 121 | throw new RuntimeException('A strategy must be set to process a route'); 122 | } 123 | 124 | return $strategy->invokeRouteCallable($this, $request); 125 | } 126 | 127 | public function setParentGroup(RouteGroup $group): self 128 | { 129 | $this->group = $group; 130 | $prefix = $this->group->getPrefix(); 131 | $path = $this->getPath(); 132 | 133 | if (strcmp($prefix, substr($path, 0, strlen($prefix))) !== 0) { 134 | $path = $prefix . $path; 135 | $this->path = $path; 136 | } 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * @param array $vars 143 | */ 144 | public function setVars(array $vars): self 145 | { 146 | $this->vars = $vars; 147 | return $this; 148 | } 149 | 150 | /** 151 | * @throws ContainerExceptionInterface 152 | * @throws NotFoundExceptionInterface 153 | */ 154 | protected function resolve(string $class, ?ContainerInterface $container = null): mixed 155 | { 156 | if ($container instanceof ContainerInterface && $container->has($class)) { 157 | return $container->get($class); 158 | } 159 | 160 | if (class_exists($class)) { 161 | return new $class(); 162 | } 163 | 164 | return $class; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/RouteCollectionInterface.php: -------------------------------------------------------------------------------- 1 | map(Request::METHOD_DELETE, $path, $handler); 21 | } 22 | 23 | public function get(string $path, callable|array|string|RequestHandlerInterface $handler): Route 24 | { 25 | return $this->map(Request::METHOD_GET, $path, $handler); 26 | } 27 | 28 | public function head(string $path, callable|array|string|RequestHandlerInterface $handler): Route 29 | { 30 | return $this->map(Request::METHOD_HEAD, $path, $handler); 31 | } 32 | 33 | public function options(string $path, callable|array|string|RequestHandlerInterface $handler): Route 34 | { 35 | return $this->map(Request::METHOD_OPTIONS, $path, $handler); 36 | } 37 | 38 | public function patch(string $path, callable|array|string|RequestHandlerInterface $handler): Route 39 | { 40 | return $this->map(Request::METHOD_PATCH, $path, $handler); 41 | } 42 | 43 | public function post(string $path, callable|array|string|RequestHandlerInterface $handler): Route 44 | { 45 | return $this->map(Request::METHOD_POST, $path, $handler); 46 | } 47 | 48 | public function put(string $path, callable|array|string|RequestHandlerInterface $handler): Route 49 | { 50 | return $this->map(Request::METHOD_PUT, $path, $handler); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/RouteConditionHandlerInterface.php: -------------------------------------------------------------------------------- 1 | host; 20 | } 21 | 22 | public function getName(): ?string 23 | { 24 | return $this->name; 25 | } 26 | 27 | public function getPort(): ?int 28 | { 29 | return $this->port; 30 | } 31 | 32 | public function getScheme(): ?string 33 | { 34 | return $this->scheme; 35 | } 36 | 37 | public function setHost(string $host): RouteConditionHandlerInterface 38 | { 39 | $this->host = $host; 40 | return $this->checkAndReturnSelf(); 41 | } 42 | 43 | public function setName(string $name): RouteConditionHandlerInterface 44 | { 45 | $this->name = $name; 46 | return $this->checkAndReturnSelf(); 47 | } 48 | 49 | public function setPort(int $port): RouteConditionHandlerInterface 50 | { 51 | $this->port = $port; 52 | return $this->checkAndReturnSelf(); 53 | } 54 | 55 | public function setScheme(string $scheme): RouteConditionHandlerInterface 56 | { 57 | $this->scheme = $scheme; 58 | return $this->checkAndReturnSelf(); 59 | } 60 | 61 | private function checkAndReturnSelf(): RouteConditionHandlerInterface 62 | { 63 | if ($this instanceof RouteConditionHandlerInterface) { 64 | return $this; 65 | } 66 | 67 | throw new RuntimeException(sprintf( 68 | 'Trait (%s) must be consumed by an instance of (%s)', 69 | __TRAIT__, 70 | RouteConditionHandlerInterface::class 71 | )); 72 | } 73 | 74 | protected function isExtraConditionMatch(Route $route, ServerRequestInterface $request): bool 75 | { 76 | // check for scheme condition 77 | $scheme = $route->getScheme(); 78 | if ($scheme !== null && $scheme !== $request->getUri()->getScheme()) { 79 | return false; 80 | } 81 | 82 | // check for domain condition 83 | $host = $route->getHost(); 84 | if ($host !== null && $host !== $request->getUri()->getHost()) { 85 | return false; 86 | } 87 | 88 | // check for port condition 89 | $port = $route->getPort(); 90 | return !($port !== null && $port !== $request->getUri()->getPort()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/RouteGroup.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 33 | $this->prefix = sprintf('/%s', ltrim($this->prefix, '/')); 34 | } 35 | 36 | public function __invoke(): void 37 | { 38 | ($this->callback)($this); 39 | } 40 | 41 | public function getPrefix(): string 42 | { 43 | return $this->prefix; 44 | } 45 | 46 | public function map( 47 | string|array $method, 48 | string $path, 49 | callable|array|string|RequestHandlerInterface $handler 50 | ): Route { 51 | $path = ($path === '/') ? $this->prefix : $this->prefix . sprintf('/%s', ltrim($path, '/')); 52 | $route = $this->collection->map($method, $path, $handler); 53 | 54 | $route->setParentGroup($this); 55 | 56 | if ($host = $this->getHost()) { 57 | $route->setHost($host); 58 | } 59 | 60 | if ($scheme = $this->getScheme()) { 61 | $route->setScheme($scheme); 62 | } 63 | 64 | if ($port = $this->getPort()) { 65 | $route->setPort($port); 66 | } 67 | 68 | if ($route->getStrategy() === null && $this->getStrategy() !== null) { 69 | $route->setStrategy($this->getStrategy()); 70 | } 71 | 72 | return $route; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Router.php: -------------------------------------------------------------------------------- 1 | '{$1:[0-9]+}', 41 | '/{(.+?):word}/' => '{$1:[a-zA-Z]+}', 42 | '/{(.+?):alphanum_dash}/' => '{$1:[a-zA-Z0-9-_]+}', 43 | '/{(.+?):slug}/' => '{$1:[a-z0-9-]+}', 44 | '/{(.+?):uuid}/' => '{$1:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}+}' 45 | ]; 46 | 47 | /** 48 | * @var Route[] 49 | */ 50 | protected array $routes = []; 51 | 52 | protected bool $routesPrepared = false; 53 | 54 | protected array $routesData = []; 55 | 56 | public function __construct(protected ?RouteCollector $routeCollector = null) 57 | { 58 | $this->routeCollector = $this->routeCollector ?? new RouteCollector( 59 | new RouteParser\Std(), 60 | new DataGenerator\GroupCountBased() 61 | ); 62 | } 63 | 64 | public function addPatternMatcher(string $alias, string $regex): self 65 | { 66 | $pattern = '/{(.+?):' . $alias . '}/'; 67 | $regex = '{$1:' . $regex . '}'; 68 | $this->patternMatchers[$pattern] = $regex; 69 | return $this; 70 | } 71 | 72 | public function group(string $prefix, callable $group): RouteGroup 73 | { 74 | $group = new RouteGroup($prefix, $group, $this); 75 | $this->groups[] = $group; 76 | return $group; 77 | } 78 | 79 | public function dispatch(ServerRequestInterface $request): ResponseInterface 80 | { 81 | if (false === $this->routesPrepared) { 82 | $this->prepareRoutes($request); 83 | } 84 | 85 | /** @var Dispatcher $dispatcher */ 86 | $dispatcher = (new Dispatcher($this->routesData))->setStrategy($this->getStrategy()); 87 | 88 | foreach ($this->getMiddlewareStack() as $middleware) { 89 | if (is_string($middleware)) { 90 | $dispatcher->lazyMiddleware($middleware); 91 | continue; 92 | } 93 | 94 | $dispatcher->middleware($middleware); 95 | } 96 | 97 | return $dispatcher->dispatchRequest($request); 98 | } 99 | 100 | public function getNamedRoute(string $name): Route 101 | { 102 | if (!$this->routesPrepared) { 103 | $this->collectGroupRoutes(); 104 | } 105 | 106 | $this->buildNameIndex(); 107 | 108 | if (isset($this->namedRoutes[$name])) { 109 | return $this->namedRoutes[$name]; 110 | } 111 | 112 | throw new InvalidArgumentException(sprintf('No route of the name (%s) exists', $name)); 113 | } 114 | 115 | public function handle(ServerRequestInterface $request): ResponseInterface 116 | { 117 | return $this->dispatch($request); 118 | } 119 | 120 | public function map( 121 | string|array $method, 122 | string $path, 123 | callable|array|string|RequestHandlerInterface $handler 124 | ): Route { 125 | $path = sprintf('/%s', ltrim($path, '/')); 126 | $route = new Route($method, $path, $handler); 127 | 128 | $this->routes[] = $route; 129 | 130 | return $route; 131 | } 132 | 133 | public function prepareRoutes(ServerRequestInterface $request): void 134 | { 135 | if ($this->getStrategy() === null) { 136 | $this->setStrategy(new ApplicationStrategy()); 137 | } 138 | 139 | $this->processGroups($request); 140 | $this->buildNameIndex(); 141 | 142 | $routes = array_merge(array_values($this->routes), array_values($this->namedRoutes)); 143 | $options = []; 144 | 145 | /** @var Route $route */ 146 | foreach ($routes as $route) { 147 | // this allows for the same route to be mapped across different routes/hosts etc 148 | if (false === $this->isExtraConditionMatch($route, $request)) { 149 | continue; 150 | } 151 | 152 | if ($route->getStrategy() === null) { 153 | $route->setStrategy($this->getStrategy()); 154 | } 155 | 156 | $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route); 157 | 158 | // global strategy must be an OPTIONS handler to automatically generate OPTIONS route 159 | if (!($this->getStrategy() instanceof OptionsHandlerInterface)) { 160 | continue; 161 | } 162 | 163 | // need a messy but useful identifier to determine what methods to respond with on OPTIONS 164 | $identifier = $route->getScheme() . static::IDENTIFIER_SEPARATOR . $route->getHost() 165 | . static::IDENTIFIER_SEPARATOR . $route->getPort() . static::IDENTIFIER_SEPARATOR . $route->getPath(); 166 | 167 | // if there is a defined OPTIONS route, do not generate one 168 | if ('OPTIONS' === $route->getMethod()) { 169 | unset($options[$identifier]); 170 | continue; 171 | } 172 | 173 | if (!isset($options[$identifier])) { 174 | $options[$identifier] = []; 175 | } 176 | 177 | $options[$identifier][] = $route->getMethod(); 178 | } 179 | 180 | $this->buildOptionsRoutes($options); 181 | 182 | $this->routesPrepared = true; 183 | $this->routesData = $this->routeCollector->getData(); 184 | } 185 | 186 | protected function buildNameIndex(): void 187 | { 188 | foreach ($this->routes as $key => $route) { 189 | if ($route->getName() !== null) { 190 | unset($this->routes[$key]); 191 | $this->namedRoutes[$route->getName()] = $route; 192 | } 193 | } 194 | } 195 | 196 | protected function buildOptionsRoutes(array $options): void 197 | { 198 | if (!($this->getStrategy() instanceof OptionsHandlerInterface)) { 199 | return; 200 | } 201 | 202 | /** @var OptionsHandlerInterface $strategy */ 203 | $strategy = $this->getStrategy(); 204 | 205 | foreach ($options as $identifier => $methods) { 206 | [$scheme, $host, $port, $path] = explode(static::IDENTIFIER_SEPARATOR, $identifier); 207 | $route = new Route('OPTIONS', $path, $strategy->getOptionsCallable($methods)); 208 | 209 | if (!empty($scheme)) { 210 | $route->setScheme($scheme); 211 | } 212 | 213 | if (!empty($host)) { 214 | $route->setHost($host); 215 | } 216 | 217 | if (!empty($port)) { 218 | $route->setPort($port); 219 | } 220 | 221 | $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route); 222 | } 223 | } 224 | 225 | protected function collectGroupRoutes(): void 226 | { 227 | foreach ($this->groups as $group) { 228 | $group(); 229 | } 230 | } 231 | 232 | protected function processGroups(ServerRequestInterface $request): void 233 | { 234 | $activePath = $request->getUri()->getPath(); 235 | 236 | foreach ($this->groups as $key => $group) { 237 | // we want to determine if we are technically in a group even if the 238 | // route is not matched so exceptions are handled correctly 239 | if ( 240 | $group->getStrategy() !== null 241 | && strncmp($activePath, $group->getPrefix(), strlen($group->getPrefix())) === 0 242 | ) { 243 | $this->setStrategy($group->getStrategy()); 244 | } 245 | 246 | unset($this->groups[$key]); 247 | $group(); 248 | } 249 | } 250 | 251 | protected function parseRoutePath(string $path): string 252 | { 253 | return preg_replace(array_keys($this->patternMatchers), array_values($this->patternMatchers), $path); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/Strategy/AbstractStrategy.php: -------------------------------------------------------------------------------- 1 | responseDecorators[] = $decorator; 16 | return $this; 17 | } 18 | 19 | protected function decorateResponse(ResponseInterface $response): ResponseInterface 20 | { 21 | foreach ($this->responseDecorators as $decorator) { 22 | $response = $decorator($response); 23 | } 24 | 25 | return $response; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Strategy/ApplicationStrategy.php: -------------------------------------------------------------------------------- 1 | throwThrowableMiddleware($exception); 21 | } 22 | 23 | public function getNotFoundDecorator(NotFoundException $exception): MiddlewareInterface 24 | { 25 | return $this->throwThrowableMiddleware($exception); 26 | } 27 | 28 | public function getThrowableHandler(): MiddlewareInterface 29 | { 30 | return new class implements MiddlewareInterface 31 | { 32 | public function process( 33 | ServerRequestInterface $request, 34 | RequestHandlerInterface $handler 35 | ): ResponseInterface { 36 | try { 37 | return $handler->handle($request); 38 | } catch (Throwable $e) { 39 | throw $e; 40 | } 41 | } 42 | }; 43 | } 44 | 45 | public function invokeRouteCallable(Route $route, ServerRequestInterface $request): ResponseInterface 46 | { 47 | $controller = $route->getCallable($this->getContainer()); 48 | $response = $controller($request, $route->getVars()); 49 | return $this->decorateResponse($response); 50 | } 51 | 52 | protected function throwThrowableMiddleware(Throwable $error): MiddlewareInterface 53 | { 54 | return new class ($error) implements MiddlewareInterface 55 | { 56 | protected $error; 57 | 58 | public function __construct(Throwable $error) 59 | { 60 | $this->error = $error; 61 | } 62 | 63 | public function process( 64 | ServerRequestInterface $request, 65 | RequestHandlerInterface $handler 66 | ): ResponseInterface { 67 | throw $this->error; 68 | } 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Strategy/JsonStrategy.php: -------------------------------------------------------------------------------- 1 | addResponseDecorator(static function (ResponseInterface $response): ResponseInterface { 23 | if (false === $response->hasHeader('content-type')) { 24 | $response = $response->withHeader('content-type', 'application/json'); 25 | } 26 | 27 | return $response; 28 | }); 29 | } 30 | 31 | public function getMethodNotAllowedDecorator(MethodNotAllowedException $exception): MiddlewareInterface 32 | { 33 | return $this->buildJsonResponseMiddleware($exception); 34 | } 35 | 36 | public function getNotFoundDecorator(NotFoundException $exception): MiddlewareInterface 37 | { 38 | return $this->buildJsonResponseMiddleware($exception); 39 | } 40 | 41 | public function getOptionsCallable(array $methods): callable 42 | { 43 | return function () use ($methods): ResponseInterface { 44 | $options = implode(', ', $methods); 45 | $response = $this->responseFactory->createResponse(); 46 | $response = $response->withHeader('allow', $options); 47 | return $response->withHeader('access-control-allow-methods', $options); 48 | }; 49 | } 50 | 51 | public function getThrowableHandler(): MiddlewareInterface 52 | { 53 | return new class ($this->responseFactory->createResponse()) implements MiddlewareInterface 54 | { 55 | protected $response; 56 | 57 | public function __construct(ResponseInterface $response) 58 | { 59 | $this->response = $response; 60 | } 61 | 62 | public function process( 63 | ServerRequestInterface $request, 64 | RequestHandlerInterface $handler 65 | ): ResponseInterface { 66 | try { 67 | return $handler->handle($request); 68 | } catch (Throwable $exception) { 69 | $response = $this->response; 70 | 71 | if ($exception instanceof Http\Exception) { 72 | return $exception->buildJsonResponse($response); 73 | } 74 | 75 | $response->getBody()->write(json_encode([ 76 | 'status_code' => 500, 77 | 'reason_phrase' => $exception->getMessage() 78 | ])); 79 | 80 | $response = $response->withAddedHeader('content-type', 'application/json'); 81 | return $response->withStatus(500, strtok($exception->getMessage(), "\n")); 82 | } 83 | } 84 | }; 85 | } 86 | 87 | public function invokeRouteCallable(Route $route, ServerRequestInterface $request): ResponseInterface 88 | { 89 | $controller = $route->getCallable($this->getContainer()); 90 | $response = $controller($request, $route->getVars()); 91 | 92 | if ($this->isJsonSerializable($response)) { 93 | $body = json_encode($response, $this->jsonFlags); 94 | $response = $this->responseFactory->createResponse(); 95 | $response->getBody()->write($body); 96 | } 97 | 98 | return $this->decorateResponse($response); 99 | } 100 | 101 | protected function buildJsonResponseMiddleware(Http\Exception $exception): MiddlewareInterface 102 | { 103 | return new class ($this->responseFactory->createResponse(), $exception) implements MiddlewareInterface 104 | { 105 | protected $response; 106 | protected $exception; 107 | 108 | public function __construct(ResponseInterface $response, Http\Exception $exception) 109 | { 110 | $this->response = $response; 111 | $this->exception = $exception; 112 | } 113 | 114 | public function process( 115 | ServerRequestInterface $request, 116 | RequestHandlerInterface $handler 117 | ): ResponseInterface { 118 | return $this->exception->buildJsonResponse($this->response); 119 | } 120 | }; 121 | } 122 | 123 | protected function isJsonSerializable($response): bool 124 | { 125 | if ($response instanceof ResponseInterface) { 126 | return false; 127 | } 128 | 129 | return (is_array($response) || is_object($response) || $response instanceof JsonSerializable); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Strategy/OptionsHandlerInterface.php: -------------------------------------------------------------------------------- 1 | strategy = $strategy; 14 | return $this; 15 | } 16 | 17 | public function getStrategy(): ?StrategyInterface 18 | { 19 | return $this->strategy; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Strategy/StrategyInterface.php: -------------------------------------------------------------------------------- 1 |