├── 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 |
--------------------------------------------------------------------------------