├── phpstan.neon ├── phpcs.xml ├── phpunit.xml ├── src ├── RequestHandler.php ├── FactoryInterface.php ├── Dispatcher.php ├── HttpErrorException.php ├── CallableHandler.php ├── RequestHandlerContainer.php ├── Factory.php └── FactoryDiscovery.php ├── .phpstan-baseline.neon ├── LICENSE ├── phpstan-baseline.neon ├── composer.json ├── .github └── workflows │ └── main.yaml ├── CONTRIBUTING.md ├── README.md └── CHANGELOG.md /phpstan.neon: -------------------------------------------------------------------------------- 1 | # 2 | # phpstan at max level leaves a mess for factory 3 | # 4 | includes: 5 | - phpstan-baseline.neon 6 | 7 | parameters: 8 | level: max 9 | checkMissingIterableValueType: false 10 | paths: 11 | - src 12 | - tests 13 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Middlewares coding standard 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | src 15 | tests 16 | 17 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tests 7 | 8 | 9 | 10 | 11 | ./src 12 | 13 | 14 | ./tests 15 | ./vendor 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/RequestHandler.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 23 | } 24 | 25 | public function handle(ServerRequestInterface $request): ResponseInterface 26 | { 27 | return call_user_func($this->callback, $request); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/FactoryInterface.php: -------------------------------------------------------------------------------- 1 | \\|string given\\.$#" 17 | count: 1 18 | path: src/FactoryDiscovery.php 19 | 20 | - 21 | message: "#^Property Middlewares\\\\Utils\\\\FactoryDiscovery\\:\\:\\$factory has no type specified\\.$#" 22 | count: 1 23 | path: src/FactoryDiscovery.php 24 | 25 | - 26 | message: "#^Unsafe usage of new static\\(\\)\\.$#" 27 | count: 1 28 | path: src/HttpErrorException.php 29 | 30 | - 31 | message: "#^Method Middlewares\\\\Utils\\\\RequestHandlerContainer\\:\\:resolve\\(\\) has no return type specified\\.$#" 32 | count: 1 33 | path: src/RequestHandlerContainer.php 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2025 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Unsafe usage of new static\\(\\)\\.$#" 5 | count: 1 6 | path: src/Dispatcher.php 7 | 8 | - 9 | message: "#^Negated boolean expression is always false\\.$#" 10 | count: 1 11 | path: src/Factory.php 12 | 13 | - 14 | message: "#^Method Middlewares\\\\Utils\\\\FactoryDiscovery\\:\\:getFactory\\(\\) has no return type specified\\.$#" 15 | count: 1 16 | path: src/FactoryDiscovery.php 17 | 18 | - 19 | message: "#^Parameter \\#1 \\$class of function class_exists expects string, array\\\\|string given\\.$#" 20 | count: 1 21 | path: src/FactoryDiscovery.php 22 | 23 | - 24 | message: "#^Property Middlewares\\\\Utils\\\\FactoryDiscovery\\:\\:\\$factory has no type specified\\.$#" 25 | count: 1 26 | path: src/FactoryDiscovery.php 27 | 28 | - 29 | message: "#^Unsafe usage of new static\\(\\)\\.$#" 30 | count: 1 31 | path: src/HttpErrorException.php 32 | 33 | - 34 | message: "#^Method Middlewares\\\\Utils\\\\RequestHandlerContainer\\:\\:resolve\\(\\) has no return type specified\\.$#" 35 | count: 1 36 | path: src/RequestHandlerContainer.php 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "middlewares/utils", 3 | "type": "library", 4 | "description": "Common utils for PSR-15 middleware packages", 5 | "license": "MIT", 6 | "keywords": [ 7 | "psr-7", 8 | "psr-15", 9 | "psr-11", 10 | "psr-17", 11 | "middleware", 12 | "http" 13 | ], 14 | "homepage": "https://github.com/middlewares/utils", 15 | "support": { 16 | "issues": "https://github.com/middlewares/utils/issues" 17 | }, 18 | "require": { 19 | "php": ">=8.1", 20 | "psr/http-message": "^1.0 || ^2.0", 21 | "psr/http-server-middleware": "^1", 22 | "psr/container": "^1.0 || ^2.0", 23 | "psr/http-factory": "^1.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^10", 27 | "phpstan/phpstan": "^2", 28 | "laminas/laminas-diactoros": "^3", 29 | "friendsofphp/php-cs-fixer": "^3", 30 | "oscarotero/php-cs-fixer-config": "^2", 31 | "squizlabs/php_codesniffer": "^3", 32 | "slim/psr7": "^1.6", 33 | "guzzlehttp/psr7": "^2.6", 34 | "sunrise/http-message": "^3.0", 35 | "nyholm/psr7": "^1.8" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Middlewares\\Utils\\": "src/" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Middlewares\\Tests\\": "tests/" 45 | } 46 | }, 47 | "scripts": { 48 | "cs": "phpcs", 49 | "cs-fix": "php-cs-fixer fix", 50 | "phpstan": "phpstan analyse", 51 | "test": "phpunit", 52 | "coverage": "phpunit --coverage-text", 53 | "coverage-html": "phpunit --coverage-html=coverage" 54 | } 55 | } -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: "testing" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | qa: 11 | name: Quality assurance 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Validate composer.json and composer.lock 19 | run: composer validate 20 | 21 | - name: Cache Composer packages 22 | id: composer-cache 23 | uses: actions/cache@v4 24 | with: 25 | path: vendor 26 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-php- 29 | 30 | - name: Install dependencies 31 | if: steps.composer-cache.outputs.cache-hit != 'true' 32 | run: composer install --prefer-dist --no-progress 33 | 34 | - name: Coding Standard 35 | run: composer run-script cs 36 | 37 | tests: 38 | name: Tests 39 | runs-on: ubuntu-latest 40 | 41 | strategy: 42 | matrix: 43 | php: 44 | - 8.1 45 | - 8.2 46 | - 8.3 47 | - 8.4 48 | 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | 53 | - name: Install PHP 54 | uses: shivammathur/setup-php@v2 55 | with: 56 | php-version: ${{ matrix.php }} 57 | 58 | - name: Cache PHP dependencies 59 | uses: actions/cache@v4 60 | with: 61 | path: vendor 62 | key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} 63 | restore-keys: ${{ runner.os }}-php-${{ matrix.php }}-composer- 64 | 65 | - name: Install dependencies 66 | run: composer install --prefer-dist --no-progress 67 | 68 | - name: Tests 69 | run: composer test 70 | -------------------------------------------------------------------------------- /src/Dispatcher.php: -------------------------------------------------------------------------------- 1 | dispatch($request); 29 | } 30 | 31 | /** 32 | * @param CallableHandler[]|MiddlewareInterface[]|callable[] $stack 33 | */ 34 | public function __construct(array $stack) 35 | { 36 | $this->stack = $stack; 37 | } 38 | 39 | public function handle(ServerRequestInterface $request): ResponseInterface 40 | { 41 | return $this->dispatch($request); 42 | } 43 | 44 | /** 45 | * Dispatches the middleware stack and returns the resulting `ResponseInterface`. 46 | */ 47 | public function dispatch(ServerRequestInterface $request): ResponseInterface 48 | { 49 | $resolved = $this->resolve(0); 50 | 51 | return $resolved->handle($request); 52 | } 53 | 54 | private function resolve(int $index): RequestHandlerInterface 55 | { 56 | return new RequestHandler(function (ServerRequestInterface $request) use ($index) { 57 | $middleware = isset($this->stack[$index]) ? $this->stack[$index] : new CallableHandler(function () { 58 | }); 59 | 60 | if ($middleware instanceof Closure) { 61 | $middleware = new CallableHandler($middleware); 62 | } 63 | 64 | if (!($middleware instanceof MiddlewareInterface)) { 65 | throw new UnexpectedValueException( 66 | sprintf('The middleware must be an instance of %s', MiddlewareInterface::class) 67 | ); 68 | } 69 | 70 | return $middleware->process($request, $this->resolve($index + 1)); 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/HttpErrorException.php: -------------------------------------------------------------------------------- 1 | */ 13 | private static $phrases = [ 14 | // CLIENT ERROR 15 | 400 => 'Bad Request', 16 | 401 => 'Unauthorized', 17 | 402 => 'Payment Required', 18 | 403 => 'Forbidden', 19 | 404 => 'Not Found', 20 | 405 => 'Method Not Allowed', 21 | 406 => 'Not Acceptable', 22 | 407 => 'Proxy Authentication Required', 23 | 408 => 'Request Time-out', 24 | 409 => 'Conflict', 25 | 410 => 'Gone', 26 | 411 => 'Length Required', 27 | 412 => 'Precondition Failed', 28 | 413 => 'Request Entity Too Large', 29 | 414 => 'Request-URI Too Large', 30 | 415 => 'Unsupported Media Type', 31 | 416 => 'Requested range not satisfiable', 32 | 417 => 'Expectation Failed', 33 | 418 => 'I\'m a teapot', 34 | 421 => 'Misdirected Request', 35 | 422 => 'Unprocessable Entity', 36 | 423 => 'Locked', 37 | 424 => 'Failed Dependency', 38 | 425 => 'Unordered Collection', 39 | 426 => 'Upgrade Required', 40 | 428 => 'Precondition Required', 41 | 429 => 'Too Many Requests', 42 | 431 => 'Request Header Fields Too Large', 43 | 444 => 'Connection Closed Without Response', 44 | 451 => 'Unavailable For Legal Reasons', 45 | // SERVER ERROR 46 | 499 => 'Client Closed Request', 47 | 500 => 'Internal Server Error', 48 | 501 => 'Not Implemented', 49 | 502 => 'Bad Gateway', 50 | 503 => 'Service Unavailable', 51 | 504 => 'Gateway Time-out', 52 | 505 => 'HTTP Version not supported', 53 | 506 => 'Variant Also Negotiates', 54 | 507 => 'Insufficient Storage', 55 | 508 => 'Loop Detected', 56 | 510 => 'Not Extended', 57 | 511 => 'Network Authentication Required', 58 | 599 => 'Network Connect Timeout Error', 59 | ]; 60 | 61 | /** @var array */ 62 | private $context = []; 63 | 64 | /** 65 | * Create and returns a new instance 66 | * 67 | * @param int $code A valid http error code 68 | * @param array $context 69 | */ 70 | public static function create(int $code = 500, array $context = [], ?Throwable $previous = null): self 71 | { 72 | if (!isset(self::$phrases[$code])) { 73 | throw new RuntimeException("Http error not valid ({$code})"); 74 | } 75 | 76 | $exception = new static(self::$phrases[$code], $code, $previous); 77 | $exception->context = $context; 78 | 79 | return $exception; 80 | } 81 | 82 | /** 83 | * @return array 84 | */ 85 | public function getContext(): array 86 | { 87 | return $this->context; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/CallableHandler.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 28 | $this->responseFactory = $responseFactory; 29 | } 30 | 31 | /** 32 | * Process a server request and return a response. 33 | * 34 | * @see RequestHandlerInterface 35 | */ 36 | public function handle(ServerRequestInterface $request): ResponseInterface 37 | { 38 | return $this->execute([$request]); 39 | } 40 | 41 | /** 42 | * Process a server request and return a response. 43 | * 44 | * @see MiddlewareInterface 45 | */ 46 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 47 | { 48 | return $this->execute([$request, $handler]); 49 | } 50 | 51 | /** 52 | * Magic method to invoke the callable directly 53 | */ 54 | public function __invoke(): ResponseInterface 55 | { 56 | return $this->execute(func_get_args()); 57 | } 58 | 59 | /** 60 | * Execute the callable. 61 | * 62 | * @param array $arguments 63 | */ 64 | private function execute(array $arguments = []): ResponseInterface 65 | { 66 | ob_start(); 67 | $level = ob_get_level(); 68 | 69 | try { 70 | $return = call_user_func_array($this->callable, $arguments); 71 | 72 | if ($return instanceof ResponseInterface) { 73 | $response = $return; 74 | $return = ''; 75 | } elseif (is_null($return) 76 | || is_scalar($return) 77 | || (is_object($return) && method_exists($return, '__toString')) 78 | ) { 79 | $responseFactory = $this->responseFactory ?: Factory::getResponseFactory(); 80 | $response = $responseFactory->createResponse(); 81 | } else { 82 | throw new UnexpectedValueException( 83 | 'The value returned must be scalar or an object with __toString method' 84 | ); 85 | } 86 | 87 | while (ob_get_level() >= $level) { 88 | $return = ob_get_clean().$return; 89 | } 90 | 91 | $return = (string) $return; 92 | $body = $response->getBody(); 93 | 94 | if ($return !== '' && $body->isWritable()) { 95 | $body->write($return); 96 | } 97 | 98 | return $response; 99 | } catch (Exception $exception) { 100 | while (ob_get_level() >= $level) { 101 | ob_end_clean(); 102 | } 103 | 104 | throw $exception; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | This project adheres to [The Code Manifesto](http://codemanifesto.com) as its guidelines for contributor interactions. 4 | 5 | ## The Code Manifesto 6 | 7 | We want to work in an ecosystem that empowers developers to reach their potential--one that encourages growth and effective collaboration. A space that is safe for all. 8 | 9 | A space such as this benefits everyone that participates in it. It encourages new developers to enter our field. It is through discussion and collaboration that we grow, and through growth that we improve. 10 | 11 | In the effort to create such a place, we hold to these values: 12 | 13 | 1. **Discrimination limits us.** This includes discrimination on the basis of race, gender, sexual orientation, gender identity, age, nationality, technology and any other arbitrary exclusion of a group of people. 14 | 2. **Boundaries honor us.** Your comfort levels are not everyone’s comfort levels. Remember that, and if brought to your attention, heed it. 15 | 3. **We are our biggest assets.** None of us were born masters of our trade. Each of us has been helped along the way. Return that favor, when and where you can. 16 | 4. **We are resources for the future.** As an extension of #3, share what you know. Make yourself a resource to help those that come after you. 17 | 5. **Respect defines us.** Treat others as you wish to be treated. Make your discussions, criticisms and debates from a position of respectfulness. Ask yourself, is it true? Is it necessary? Is it constructive? Anything less is unacceptable. 18 | 6. **Reactions require grace.** Angry responses are valid, but abusive language and vindictive actions are toxic. When something happens that offends you, handle it assertively, but be respectful. Escalate reasonably, and try to allow the offender an opportunity to explain themselves, and possibly correct the issue. 19 | 7. **Opinions are just that: opinions.** Each and every one of us, due to our background and upbringing, have varying opinions. That is perfectly acceptable. Remember this: if you respect your own opinions, you should respect the opinions of others. 20 | 8. **To err is human.** You might not intend it, but mistakes do happen and contribute to build experience. Tolerate honest mistakes, and don't hesitate to apologize if you make one yourself. 21 | 22 | ## How to contribute 23 | 24 | This is a collaborative effort. We welcome all contributions submitted as pull requests. 25 | 26 | (Contributions on wording & style are also welcome.) 27 | 28 | ### Bugs 29 | 30 | A bug is a demonstrable problem that is caused by the code in the repository. Good bug reports are extremely helpful – thank you! 31 | 32 | Please try to be as detailed as possible in your report. Include specific information about the environment – version of PHP, etc, and steps required to reproduce the issue. 33 | 34 | ### Pull Requests 35 | 36 | Good pull requests – patches, improvements, new features – are a fantastic help. Before create a pull request, please follow these instructions: 37 | 38 | * The code must follow the [PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). Run `composer cs-fix` to fix your code before commit. 39 | * Write tests 40 | * Document any change in `README.md` and `CHANGELOG.md` 41 | * One pull request per feature. If you want to do more than one thing, send multiple pull request 42 | 43 | ### Runing tests 44 | 45 | ```sh 46 | composer test 47 | ``` 48 | 49 | To get code coverage information execute the following comand: 50 | 51 | ```sh 52 | composer coverage 53 | ``` 54 | 55 | Then, open the `./coverage/index.html` file in your browser. 56 | -------------------------------------------------------------------------------- /src/RequestHandlerContainer.php: -------------------------------------------------------------------------------- 1 | constructorArguments = $constructorArguments; 28 | } 29 | 30 | public function has($id): bool 31 | { 32 | $id = $this->split($id); 33 | 34 | if (is_string($id)) { 35 | return function_exists($id) || class_exists($id); 36 | } 37 | 38 | return class_exists($id[0]) && method_exists($id[0], $id[1]); 39 | } 40 | 41 | /** 42 | * @param class-string|string $id 43 | * @return RequestHandlerInterface 44 | */ 45 | public function get($id) 46 | { 47 | try { 48 | $handler = $this->resolve($id); 49 | 50 | if ($handler instanceof RequestHandlerInterface) { 51 | return $handler; 52 | } 53 | 54 | return new CallableHandler($handler); 55 | } catch (NotFoundExceptionInterface $exception) { 56 | throw $exception; 57 | } catch (Exception $exception) { 58 | $message = sprintf('Error getting the handler %s', $id); 59 | throw new class($message, 0, $exception) extends Exception implements ContainerExceptionInterface { 60 | }; 61 | } 62 | } 63 | 64 | /** 65 | * @param class-string|string $handler 66 | * @return mixed 67 | */ 68 | protected function resolve(string $handler) 69 | { 70 | $handler = $this->split($handler); 71 | 72 | if (is_string($handler)) { 73 | return function_exists($handler) ? $handler : $this->createClass($handler); 74 | } 75 | 76 | list($class, $method) = $handler; 77 | 78 | if ((new ReflectionMethod($class, $method))->isStatic()) { 79 | return $handler; 80 | } 81 | 82 | return [$this->createClass($class), $method]; 83 | } 84 | 85 | /** 86 | * Returns the instance of a class. 87 | */ 88 | protected function createClass(string $className): object 89 | { 90 | if (!class_exists($className)) { 91 | $message = sprintf('The class %s does not exists', $className); 92 | throw new class($message) extends Exception implements NotFoundExceptionInterface { 93 | }; 94 | } 95 | 96 | $reflection = new ReflectionClass($className); 97 | 98 | if ($reflection->hasMethod('__construct')) { 99 | return $reflection->newInstanceArgs($this->constructorArguments); 100 | } 101 | 102 | return $reflection->newInstance(); 103 | } 104 | 105 | /** 106 | * Slit a string to an array 107 | * 108 | * @return string|string[] 109 | */ 110 | protected function split(string $string) 111 | { 112 | //ClassName/Service::method 113 | if (strpos($string, '::') === false) { 114 | return $string; 115 | } 116 | 117 | return explode('::', $string, 2); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | getRequestFactory(); 44 | } 45 | 46 | /** 47 | * @param UriInterface|string $uri 48 | */ 49 | public static function createRequest(string $method, $uri): RequestInterface 50 | { 51 | return self::getRequestFactory()->createRequest($method, $uri); 52 | } 53 | 54 | public static function getResponseFactory(): ResponseFactoryInterface 55 | { 56 | return self::getFactory()->getResponseFactory(); 57 | } 58 | 59 | public static function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface 60 | { 61 | return self::getResponseFactory()->createResponse($code, $reasonPhrase); 62 | } 63 | 64 | public static function getServerRequestFactory(): ServerRequestFactoryInterface 65 | { 66 | return self::getFactory()->getServerRequestFactory(); 67 | } 68 | 69 | /** 70 | * @param UriInterface|string $uri 71 | * @param array $serverParams 72 | */ 73 | public static function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface 74 | { 75 | return self::getServerRequestFactory()->createServerRequest($method, $uri, $serverParams); 76 | } 77 | 78 | public static function getStreamFactory(): StreamFactoryInterface 79 | { 80 | return self::getFactory()->getStreamFactory(); 81 | } 82 | 83 | public static function createStream(string $content = ''): StreamInterface 84 | { 85 | return self::getStreamFactory()->createStream($content); 86 | } 87 | 88 | public static function getUploadedFileFactory(): UploadedFileFactoryInterface 89 | { 90 | return self::getFactory()->getUploadedFileFactory(); 91 | } 92 | 93 | public static function createUploadedFile( 94 | StreamInterface $stream, 95 | ?int $size = null, 96 | int $error = \UPLOAD_ERR_OK, 97 | ?string $filename = null, 98 | ?string $mediaType = null 99 | ): UploadedFileInterface { 100 | return self::getUploadedFileFactory()->createUploadedFile($stream, $size, $error, $filename, $mediaType); 101 | } 102 | 103 | public static function getUriFactory(): UriFactoryInterface 104 | { 105 | return self::getFactory()->getUriFactory(); 106 | } 107 | 108 | public static function createUri(string $uri = ''): UriInterface 109 | { 110 | return self::getUriFactory()->createUri($uri); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # middlewares/utils 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE) 5 | ![Testing][ico-ga] 6 | [![Total Downloads][ico-downloads]][link-downloads] 7 | 8 | Common utilities used by the middlewares' packages: 9 | 10 | * [Factory](#factory) 11 | * [Dispatcher](#dispatcher) 12 | * [CallableHandler](#callablehandler) 13 | * [HttpErrorException](#httperrorexception) 14 | 15 | ## Installation 16 | 17 | This package is installable and autoloadable via Composer as [middlewares/utils](https://packagist.org/packages/middlewares/utils). 18 | 19 | ```sh 20 | composer require middlewares/utils 21 | ``` 22 | 23 | ## Factory 24 | 25 | Used to create PSR-7 and PSR-17 instances. 26 | Detects automatically [Diactoros](https://github.com/laminas/laminas-diactoros), [Guzzle](https://github.com/guzzle/psr7), [Slim](https://github.com/slimphp/Slim), [Nyholm/psr7](https://github.com/Nyholm/psr7) and [Sunrise](https://github.com/sunrise-php) but you can register a different factory using the [psr/http-factory](https://github.com/php-fig/http-factory) interface. 27 | 28 | ```php 29 | use Middlewares\Utils\Factory; 30 | use Middlewares\Utils\FactoryDiscovery; 31 | 32 | // Create PSR-7 instances 33 | $request = Factory::createRequest('GET', '/'); 34 | $serverRequest = Factory::createServerRequest('GET', '/'); 35 | $response = Factory::createResponse(200); 36 | $stream = Factory::createStream('Hello world'); 37 | $uri = Factory::createUri('http://example.com'); 38 | $uploadedFile = Factory::createUploadedFile($stream); 39 | 40 | // Get PSR-17 instances (factories) 41 | $requestFactory = Factory::getRequestFactory(); 42 | $serverRequestFactory = Factory::getServerRequestFactory(); 43 | $responseFactory = Factory::getResponseFactory(); 44 | $streamFactory = Factory::getStreamFactory(); 45 | $uriFactory = Factory::getUriFactory(); 46 | $uploadedFileFactory = Factory::getUploadedFileFactory(); 47 | 48 | // By default, use the FactoryDiscovery class that detects diactoros, guzzle, slim, nyholm and sunrise (in this order of priority), 49 | // but you can change it and add other libraries 50 | 51 | Factory::setFactory(new FactoryDiscovery( 52 | 'MyApp\Psr17Factory', 53 | FactoryDiscovery::SLIM, 54 | FactoryDiscovery::GUZZLE, 55 | FactoryDiscovery::DIACTOROS 56 | )); 57 | 58 | //And also register directly an initialized factory 59 | Factory::getFactory()->setResponseFactory(new FooResponseFactory()); 60 | 61 | $fooResponse = Factory::createResponse(); 62 | ``` 63 | 64 | ## Dispatcher 65 | 66 | Minimalist PSR-15 compatible dispatcher. Used for testing purposes. 67 | 68 | ```php 69 | use Middlewares\Utils\Dispatcher; 70 | 71 | $response = Dispatcher::run([ 72 | new Middleware1(), 73 | new Middleware2(), 74 | new Middleware3(), 75 | function ($request, $next) { 76 | $response = $next->handle($request); 77 | return $response->withHeader('X-Foo', 'Bar'); 78 | } 79 | ]); 80 | ``` 81 | 82 | ## CallableHandler 83 | 84 | To resolve and execute a callable. It can be used as a middleware, server request handler or a callable: 85 | 86 | ```php 87 | use Middlewares\Utils\CallableHandler; 88 | 89 | $callable = new CallableHandler(function () { 90 | return 'Hello world'; 91 | }); 92 | 93 | $response = $callable(); 94 | 95 | echo $response->getBody(); //Hello world 96 | ``` 97 | 98 | ## HttpErrorException 99 | 100 | General purpose exception used to represent HTTP errors. 101 | 102 | ```php 103 | use Middlewares\Utils\HttpErrorException; 104 | 105 | try { 106 | $context = ['problem' => 'Something bad happened']; 107 | throw HttpErrorException::create(500, $context); 108 | } catch (HttpErrorException $exception) { 109 | $context = $exception->getContext(); 110 | } 111 | ``` 112 | 113 | --- 114 | 115 | Please see [CHANGELOG](CHANGELOG.md) for more information about recent changes and [CONTRIBUTING](CONTRIBUTING.md) for contributing details. 116 | 117 | The MIT License (MIT). Please see [LICENSE](LICENSE) for more information. 118 | 119 | [ico-version]: https://img.shields.io/packagist/v/middlewares/utils.svg?style=flat-square 120 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 121 | [ico-ga]: https://github.com/middlewares/utils/workflows/testing/badge.svg 122 | [ico-downloads]: https://img.shields.io/packagist/dt/middlewares/utils.svg?style=flat-square 123 | 124 | [link-packagist]: https://packagist.org/packages/middlewares/utils 125 | [link-downloads]: https://packagist.org/packages/middlewares/utils 126 | -------------------------------------------------------------------------------- /src/FactoryDiscovery.php: -------------------------------------------------------------------------------- 1 | */ 20 | public const DIACTOROS = [ 21 | 'request' => '\Laminas\Diactoros\RequestFactory', 22 | 'response' => '\Laminas\Diactoros\ResponseFactory', 23 | 'serverRequest' => '\Laminas\Diactoros\ServerRequestFactory', 24 | 'stream' => '\Laminas\Diactoros\StreamFactory', 25 | 'uploadedFile' => '\Laminas\Diactoros\UploadedFileFactory', 26 | 'uri' => '\Laminas\Diactoros\UriFactory', 27 | ]; 28 | 29 | /** @var string */ 30 | public const GUZZLE = '\GuzzleHttp\Psr7\HttpFactory'; 31 | 32 | /** @var array */ 33 | public const SLIM = [ 34 | 'request' => '\Slim\Psr7\Factory\RequestFactory', 35 | 'response' => '\Slim\Psr7\Factory\ResponseFactory', 36 | 'serverRequest' => '\Slim\Psr7\Factory\ServerRequestFactory', 37 | 'stream' => '\Slim\Psr7\Factory\StreamFactory', 38 | 'uploadedFile' => '\Slim\Psr7\Factory\UploadedFileFactory', 39 | 'uri' => '\Slim\Psr7\Factory\UriFactory', 40 | ]; 41 | 42 | /** @var string */ 43 | public const NYHOLM = '\Nyholm\Psr7\Factory\Psr17Factory'; 44 | 45 | /** @var array */ 46 | public const SUNRISE = [ 47 | 'request' => '\Sunrise\Http\Message\RequestFactory', 48 | 'response' => '\Sunrise\Http\Message\ResponseFactory', 49 | 'serverRequest' => '\Sunrise\Http\Message\ServerRequestFactory', 50 | 'stream' => '\Sunrise\Http\Message\StreamFactory', 51 | 'uploadedFile' => '\Sunrise\Http\Message\UploadedFileFactory', 52 | 'uri' => '\Sunrise\Http\Message\UriFactory', 53 | ]; 54 | 55 | /** @var array */ 56 | private $strategies = [ 57 | self::DIACTOROS, 58 | self::GUZZLE, 59 | self::SLIM, 60 | self::NYHOLM, 61 | self::SUNRISE, 62 | ]; 63 | 64 | private $factory; 65 | 66 | /** @var array */ 67 | private $factories = []; 68 | 69 | /** 70 | * @param array $strategies 71 | */ 72 | public function __construct(...$strategies) 73 | { 74 | if (!empty($strategies)) { 75 | $this->strategies = $strategies; 76 | } 77 | } 78 | 79 | /** 80 | * Get the strategies 81 | * 82 | * @return array 83 | */ 84 | public function getStrategies(): array 85 | { 86 | return $this->strategies; 87 | } 88 | 89 | public function setRequestFactory(RequestFactoryInterface $requestFactory): void 90 | { 91 | $this->factories['request'] = $requestFactory; 92 | } 93 | 94 | public function getRequestFactory(): RequestFactoryInterface 95 | { 96 | return $this->getFactory('request'); 97 | } 98 | 99 | public function setResponseFactory(ResponseFactoryInterface $responseFactory): void 100 | { 101 | $this->factories['response'] = $responseFactory; 102 | } 103 | 104 | public function getResponseFactory(): ResponseFactoryInterface 105 | { 106 | return $this->getFactory('response'); 107 | } 108 | 109 | public function setServerRequestFactory(ServerRequestFactoryInterface $serverRequestFactory): void 110 | { 111 | $this->factories['serverRequest'] = $serverRequestFactory; 112 | } 113 | 114 | public function getServerRequestFactory(): ServerRequestFactoryInterface 115 | { 116 | return $this->getFactory('serverRequest'); 117 | } 118 | 119 | public function setStreamFactory(StreamFactoryInterface $streamFactory): void 120 | { 121 | $this->factories['stream'] = $streamFactory; 122 | } 123 | 124 | public function getStreamFactory(): StreamFactoryInterface 125 | { 126 | return $this->getFactory('stream'); 127 | } 128 | 129 | public function setUploadedFileFactory(UploadedFileFactoryInterface $uploadedFileFactory): void 130 | { 131 | $this->factories['uploadedFile'] = $uploadedFileFactory; 132 | } 133 | 134 | public function getUploadedFileFactory(): UploadedFileFactoryInterface 135 | { 136 | return $this->getFactory('uploadedFile'); 137 | } 138 | 139 | public function setUriFactory(UriFactoryInterface $uriFactory): void 140 | { 141 | $this->factories['uri'] = $uriFactory; 142 | } 143 | 144 | public function getUriFactory(): UriFactoryInterface 145 | { 146 | return $this->getFactory('uri'); 147 | } 148 | 149 | /** 150 | * Create the PSR-17 factories or throw an exception 151 | */ 152 | private function getFactory(string $type) 153 | { 154 | if (!empty($this->factories[$type])) { 155 | return $this->factories[$type]; 156 | } 157 | 158 | if (!empty($this->factory)) { 159 | return $this->factories[$type] = $this->factory; 160 | } 161 | 162 | foreach ($this->strategies as $className) { 163 | if (is_array($className) && isset($className[$type])) { 164 | $className = $className[$type]; 165 | if (class_exists($className)) { 166 | return $this->factories[$type] = new $className(); 167 | } 168 | 169 | continue; 170 | } 171 | 172 | /* @phpstan-ignore-next-line */ 173 | if (!class_exists($className)) { 174 | continue; 175 | } 176 | 177 | return $this->factories[$type] = $this->factory = new $className(); 178 | } 179 | 180 | throw new RuntimeException(sprintf('No PSR-17 factory detected to create a %s', $type)); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [4.0.2] - 2025-01-23 8 | ### Fixed 9 | - PHP 8.4 deprecation errors (implicitly nullable parameter) [#30] 10 | 11 | ## [4.0.1] - 2024-11-21 12 | ### Fixed 13 | - Support for Php 8.4 [#29]. 14 | 15 | ## [4.0.0] - 2023-12-17 16 | ### Added 17 | - Support for psr/http-message 2.x (if you don't use slim/psr7 or sunrise/http-message as factory) 18 | 19 | ### Changed 20 | - Updated dependencies and tests 21 | 22 | ### Removed 23 | - Support for PHP prior to 8.1. 24 | 25 | ## [3.3.0] - 2021-07-04 26 | ### Added 27 | - Support for psr/container v2.x [#26] 28 | 29 | ## [3.2.0] - 2020-11-30 30 | ### Fixed 31 | - Added support for PHP 8 [#22] [#23] 32 | 33 | ## [3.1.0] - 2020-01-19 34 | ### Changed 35 | - Zend Diactoros is deprecated, switched to Laminas Diactoros [#20], [#21]. 36 | THIS IS A BREAKING CHANGE, so, if you want to keep using Zend Diactoros, you should configure the `Factory` as follows: 37 | ```php 38 | Factory::setFactory( 39 | new FactoryDiscovery([ 40 | 'request' => 'Zend\Diactoros\RequestFactory', 41 | 'response' => 'Zend\Diactoros\ResponseFactory', 42 | 'serverRequest' => 'Zend\Diactoros\ServerRequestFactory', 43 | 'stream' => 'Zend\Diactoros\StreamFactory', 44 | 'uploadedFile' => 'Zend\Diactoros\UploadedFileFactory', 45 | 'uri' => 'Zend\Diactoros\UriFactory', 46 | ]) 47 | ); 48 | ``` 49 | 50 | ## [3.0.1] - 2019-11-29 51 | ### Fixed 52 | - Moved a dependency to dev 53 | - Updated docs 54 | 55 | ## [3.0.0] - 2019-11-29 56 | ### Added 57 | - Added `FactoryInterface` that returns all PSR-17 factories 58 | - Added `FactoryDiscovery` class to discover automatically PSR-17 implementation libraries 59 | - Added `Factory::getFactory()` and `Factory::setFactory()` to set manually PSR-17 factories 60 | - Added `Factory::getResponseFactory()` 61 | - Added `Factory::getRequestFactory()` 62 | - Added `Factory::getServerRequestFactory()` 63 | - Added `Factory::getStreamFactory()` 64 | - Added `Factory::getUriFactory()` 65 | - Added `Factory::getUploadedFileFactory()` 66 | - Added `Sunrise` to the list of factories detected automatically 67 | 68 | ### Removed 69 | - Support for PHP 7.0 and 7.1 70 | - `Factory::setStrategy` 71 | - `HttpErrorException::setContext` method, to make the exception class inmutable 72 | - Traits `HasResponseFactory` and `HasStreamFactory` 73 | 74 | ## [2.2.0] - 2019-03-05 75 | ### Added 76 | - `Middlewares\Utils\Dispatcher` implements `RequestHandlerInterface` [#16], [#17] 77 | 78 | ## [2.1.1] - 2018-08-11 79 | ### Added 80 | - Added `Nyholm\Psr7` to the list of factories detected automatically 81 | 82 | ## [2.1.0] - 2018-08-02 83 | ### Added 84 | - New trait `HasResponseFactory` used by many middlewares that need to configure the PSR-17 response factory. 85 | - New trait `HasStreamFactory` used by many middlewares that need to configure the PSR-17 stream factory. 86 | 87 | ## [2.0.0] - 2018-08-01 88 | ### Added 89 | - New methods added to `Factory` to return PSR-17 factories: `getResponseFactory`, `getServerRequestFactory`, `getStreamFactory` and `getUriFactory`. 90 | - New method `Factory::setStrategies()` to configure the priority order of the Diactoros, Guzzle and Slim factories or register new classes. 91 | - Added a second argument to `Callablehandler` constructor to pass a response factory 92 | 93 | ### Changed 94 | - Exchanged abandoned `http-interop/http-factory` with `psr/http-factory` 95 | - Changed the signature of `Factory::createServerRequest()` to be aligned with PSR-17 96 | - Changed the signature of `Factory::createStream()` to be aligned with PSR-17 97 | - Changed the signature of `Factory::createResponse()` to be aligned with PSR-17 98 | 99 | ## [1.2.0] - 2018-07-17 100 | ### Changed 101 | - Updated `http-interop/http-factory` to `0.4` 102 | 103 | ## [1.1.0] - 2018-06-25 104 | ### Added 105 | - Imported `HttpErrorException` from error handler middleware 106 | 107 | ## [1.0.0] - 2018-01-24 108 | ### Changed 109 | - Replaced `http-interop/http-server-middleware` with `psr/http-server-middleware`. 110 | 111 | ### Removed 112 | - Removed `Middlewares\Utils\Helpers` because contains just one helper and it's no longer needed. 113 | 114 | ## [0.14.0] - 2017-12-16 115 | ### Added 116 | - New class `RequestHandlerContainer` implementing PSR-11 to resolve handlers in any format (classes, callables) and return PSR-15 `RequestHandlerInterface` instances. This can be used to resolve router handlers, for example. 117 | 118 | ### Changed 119 | - The signature of `CallableHandler` was simplified. Removed `$resolver` and `$arguments` in the constructor. 120 | 121 | ### Removed 122 | - Deleted all callable resolvers classes. Use the `RequestHandlerContainer`, or any other PSR-11 implementation. 123 | 124 | ## [0.13.0] - 2017-11-16 125 | ### Changed 126 | - The minimum PHP version supported is 7.0 127 | - Replaced `http-interop/http-middleware` with `http-interop/http-server-middleware`. 128 | - Changed `Middlewares\Utils\CallableHandler` signature. Now it is instantiable and can be used as middleware and server request handler. 129 | 130 | ### Removed 131 | - `Middlewares\Utils\CallableMiddleware`. Use `Middlewares\Utils\CallableHandler` instead. 132 | 133 | ## [0.12.0] - 2017-09-18 134 | ### Changed 135 | - Append `.dist` suffix to phpcs.xml and phpunit.xml files 136 | - Changed the configuration of phpcs and php_cs 137 | - Upgraded phpunit to the latest version and improved its config file 138 | - Updated `http-interop/http-middleware` to `0.5` 139 | 140 | ## [0.11.1] - 2017-05-06 141 | ### Changed 142 | - `Middlewares\Utils\CallableHandler` expects one of the following values returned by the callable: 143 | * A `Psr\Http\Message\ResponseInterface` 144 | * `null` or scalar 145 | * an object with `__toString` method implemented 146 | Otherwise, throws an `UnexpectedValueException` 147 | - `Middlewares\Helpers::fixContentLength` only modifies or removes the `Content-Length` header, but does not add it if didn't exist previously. 148 | 149 | ## [0.11.0] - 2017-03-25 150 | ### Added 151 | - New class `Middlewares\Utils\Helpers` with common helpers to manipulate PSR-7 messages 152 | - New helper `Middlewares\Utils\Helpers::fixContentLength` used to add/modify/remove the `Content-Length` header of a http message. 153 | 154 | ### Changed 155 | - Updated `http-interop/http-factory` to `0.3` 156 | 157 | ## [0.10.1] - 2017-02-27 158 | ### Fixed 159 | - Fixed changelog file 160 | 161 | ## [0.10.0] - 2017-02-27 162 | ### Changed 163 | - Replaced deprecated `container-interop` by `psr/contaienr` (PSR-11). 164 | - `Middlewares\Utils\Dispatcher` throws exceptions if the middlewares does not implement `Interop\Http\ServerMiddleware\MiddlewareInterface` or does not return an instance of `Psr\Http\Message\ResponseInterface`. 165 | - Moved the default factories to `Middlewares\Utils\Factory` namespace. 166 | - Minor code improvements. 167 | 168 | ## [0.9.0] - 2017-02-05 169 | ### Added 170 | - Callable resolves to create callables from various representations 171 | 172 | ### Removed 173 | - `Middlewares\Utils\CallableHandler::resolve` 174 | 175 | ## [0.8.0] - 2016-12-22 176 | ### Changed 177 | - Updated `http-interop/http-middleware` to `0.4` 178 | - Updated `friendsofphp/php-cs-fixer` to `2.0` 179 | 180 | ## [0.7.0] - 2016-12-06 181 | ### Added 182 | - New static helper `Middlewares\Utils\Dispatcher::run` to create and dispatch a request easily 183 | 184 | ## [0.6.1] - 2016-12-06 185 | ### Fixed 186 | - Ensure that the body of the serverRequest is writable and seekable. 187 | 188 | ## [0.6.0] - 2016-12-06 189 | ### Added 190 | - ServerRequest factory 191 | - `Middlewares\Utils\Dispatcher` accepts `Closure` as middleware components 192 | 193 | ### Changed 194 | - `Middlewares\Utils\Dispatcher` creates automatically a response if the stack is exhausted 195 | 196 | ## [0.5.0] - 2016-11-22 197 | ### Added 198 | - `Middlewares\Utils\CallableMiddleware` class, to create middlewares from callables 199 | - `Middlewares\Utils\Dispatcher` class, to execute the middleware stack and return a response. 200 | 201 | ## [0.4.0] - 2016-11-13 202 | ### Changed 203 | - Updated `http-interop/http-factory` to `0.2` 204 | 205 | ## [0.3.1] - 2016-10-03 206 | ### Fixed 207 | - Bug in CallableHandler that resolve to the declaring class of a method instead the final class. 208 | 209 | ## [0.3.0] - 2016-10-03 210 | ### Added 211 | - `Middlewares\Utils\CallableHandler` class, allowing to resolve and execute callables safely. 212 | 213 | ## [0.2.0] - 2016-10-01 214 | ### Added 215 | - Uri factory 216 | 217 | ## [0.1.0] - 2016-09-30 218 | ### Added 219 | - Response factory 220 | - Stream factory 221 | 222 | [#16]: https://github.com/middlewares/utils/issues/16 223 | [#17]: https://github.com/middlewares/utils/issues/17 224 | [#20]: https://github.com/middlewares/utils/issues/20 225 | [#21]: https://github.com/middlewares/utils/issues/21 226 | [#22]: https://github.com/middlewares/utils/issues/22 227 | [#23]: https://github.com/middlewares/utils/issues/23 228 | [#26]: https://github.com/middlewares/utils/issues/26 229 | [#29]: https://github.com/middlewares/utils/issues/29 230 | [#30]: https://github.com/middlewares/utils/issues/30 231 | 232 | [4.0.2]: https://github.com/middlewares/utils/compare/v4.0.1...v4.0.2 233 | [4.0.1]: https://github.com/middlewares/utils/compare/v4.0.0...v4.0.1 234 | [4.0.0]: https://github.com/middlewares/utils/compare/v3.3.0...v4.0.0 235 | [3.3.0]: https://github.com/middlewares/utils/compare/v3.2.0...v3.3.0 236 | [3.2.0]: https://github.com/middlewares/utils/compare/v3.1.0...v3.2.0 237 | [3.1.0]: https://github.com/middlewares/utils/compare/v3.0.1...v3.1.0 238 | [3.0.1]: https://github.com/middlewares/utils/compare/v3.0.0...v3.0.1 239 | [3.0.0]: https://github.com/middlewares/utils/compare/v2.2.0...v3.0.0 240 | [2.2.0]: https://github.com/middlewares/utils/compare/v2.1.1...v2.2.0 241 | [2.1.1]: https://github.com/middlewares/utils/compare/v2.1.0...v2.1.1 242 | [2.1.0]: https://github.com/middlewares/utils/compare/v2.0.0...v2.1.0 243 | [2.0.0]: https://github.com/middlewares/utils/compare/v1.2.0...v2.0.0 244 | [1.2.0]: https://github.com/middlewares/utils/compare/v1.1.0...v1.2.0 245 | [1.1.0]: https://github.com/middlewares/utils/compare/v1.0.0...v1.1.0 246 | [1.0.0]: https://github.com/middlewares/utils/compare/v0.14.0...v1.0.0 247 | [0.14.0]: https://github.com/middlewares/utils/compare/v0.13.0...v0.14.0 248 | [0.13.0]: https://github.com/middlewares/utils/compare/v0.12.0...v0.13.0 249 | [0.12.0]: https://github.com/middlewares/utils/compare/v0.11.1...v0.12.0 250 | [0.11.1]: https://github.com/middlewares/utils/compare/v0.11.0...v0.11.1 251 | [0.11.0]: https://github.com/middlewares/utils/compare/v0.10.1...v0.11.0 252 | [0.10.1]: https://github.com/middlewares/utils/compare/v0.10.0...v0.10.1 253 | [0.10.0]: https://github.com/middlewares/utils/compare/v0.9.0...v0.10.0 254 | [0.9.0]: https://github.com/middlewares/utils/compare/v0.8.0...v0.9.0 255 | [0.8.0]: https://github.com/middlewares/utils/compare/v0.7.0...v0.8.0 256 | [0.7.0]: https://github.com/middlewares/utils/compare/v0.6.1...v0.7.0 257 | [0.6.1]: https://github.com/middlewares/utils/compare/v0.6.0...v0.6.1 258 | [0.6.0]: https://github.com/middlewares/utils/compare/v0.5.0...v0.6.0 259 | [0.5.0]: https://github.com/middlewares/utils/compare/v0.4.0...v0.5.0 260 | [0.4.0]: https://github.com/middlewares/utils/compare/v0.3.1...v0.4.0 261 | [0.3.1]: https://github.com/middlewares/utils/compare/v0.3.0...v0.3.1 262 | [0.3.0]: https://github.com/middlewares/utils/compare/v0.2.0...v0.3.0 263 | [0.2.0]: https://github.com/middlewares/utils/compare/v0.1.0...v0.2.0 264 | [0.1.0]: https://github.com/middlewares/utils/releases/tag/v0.1.0 265 | --------------------------------------------------------------------------------