├── COPYRIGHT.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── EmptyPipelineHandler.php ├── Exception ├── EmptyPipelineException.php ├── ExceptionInterface.php ├── MiddlewarePipeNextHandlerAlreadyCalledException.php ├── MissingResponseException.php └── MissingResponsePrototypeException.php ├── Handler └── NotFoundHandler.php ├── IterableMiddlewarePipeInterface.php ├── Middleware ├── CallableMiddlewareDecorator.php ├── DoublePassMiddlewareDecorator.php ├── ErrorHandler.php ├── ErrorResponseGenerator.php ├── HostMiddlewareDecorator.php ├── OriginalMessages.php ├── PathMiddlewareDecorator.php └── RequestHandlerMiddleware.php ├── MiddlewarePipe.php ├── MiddlewarePipeInterface.php ├── Next.php ├── Utils.php └── functions ├── double-pass-middleware.php ├── host.php ├── middleware.php └── path.php /COPYRIGHT.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/) 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | - Neither the name of Laminas Foundation nor the names of its contributors may 14 | be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laminas-stratigility 2 | 3 | [![Build Status](https://github.com/laminas/laminas-stratigility/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/laminas/laminas-stratigility/actions/workflows/continuous-integration.yml) 4 | [![Type Coverage](https://shepherd.dev/github/laminas/laminas-stratigility/coverage.svg)](https://shepherd.dev/github/laminas/laminas-stratigility) 5 | 6 | > ## 🇷🇺 Русским гражданам 7 | > 8 | > Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм. 9 | > 10 | > У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую. 11 | > 12 | > Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!" 13 | > 14 | > ## 🇺🇸 To Citizens of Russia 15 | > 16 | > We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism. 17 | > 18 | > One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences. 19 | > 20 | > You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!" 21 | 22 | > From "Strata", Latin for "layer", and "agility". 23 | 24 | This package supersedes and replaces [phly/conduit](https://github.com/phly/conduit). 25 | 26 | Stratigility is a port of [Sencha Connect](https://github.com/senchalabs/connect) 27 | to PHP. It allows you to create and dispatch middleware pipelines. 28 | 29 | - File issues at https://github.com/laminas/laminas-stratigility/issues 30 | - Issue patches to https://github.com/laminas/laminas-stratigility/pulls 31 | - Documentation is at https://docs.laminas.dev/laminas-stratigility/ 32 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laminas/laminas-stratigility", 3 | "description": "PSR-7 middleware foundation for building and dispatching middleware pipelines", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "laminas", 7 | "http", 8 | "psr-7", 9 | "psr-15", 10 | "psr-17", 11 | "middleware" 12 | ], 13 | "homepage": "https://laminas.dev", 14 | "support": { 15 | "docs": "https://docs.laminas.dev/laminas-stratigility/", 16 | "issues": "https://github.com/laminas/laminas-stratigility/issues", 17 | "source": "https://github.com/laminas/laminas-stratigility", 18 | "rss": "https://github.com/laminas/laminas-stratigility/releases.atom", 19 | "chat": "https://laminas.dev/chat", 20 | "forum": "https://discourse.laminas.dev" 21 | }, 22 | "config": { 23 | "sort-packages": true, 24 | "platform": { 25 | "php": "8.1.99" 26 | }, 27 | "allow-plugins": { 28 | "dealerdirect/phpcodesniffer-composer-installer": true 29 | } 30 | }, 31 | "extra": {}, 32 | "require": { 33 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", 34 | "fig/http-message-util": "^1.1", 35 | "laminas/laminas-escaper": "^2.10.0", 36 | "psr/http-factory": "^1.0.2", 37 | "psr/http-message": "^1.0 || ^2.0", 38 | "psr/http-server-middleware": "^1.0.2" 39 | }, 40 | "require-dev": { 41 | "laminas/laminas-coding-standard": "~3.1.0", 42 | "laminas/laminas-diactoros": "^2.25 || ^3.6.0", 43 | "phpunit/phpunit": "^10.5.46", 44 | "psalm/plugin-phpunit": "^0.19.5", 45 | "vimeo/psalm": "^6.10.3" 46 | }, 47 | "conflict": { 48 | "zendframework/zend-stratigility": "*" 49 | }, 50 | "suggest": { 51 | "psr/http-message-implementation": "Please install a psr/http-message implementation to consume Stratigility; e.g., laminas/laminas-diactoros", 52 | "psr/http-factory-implementation": "Please install a psr/http-factory implementation to consume Stratigility; e.g., laminas/laminas-diactoros" 53 | }, 54 | "autoload": { 55 | "files": [ 56 | "src/functions/double-pass-middleware.php", 57 | "src/functions/host.php", 58 | "src/functions/middleware.php", 59 | "src/functions/path.php" 60 | ], 61 | "psr-4": { 62 | "Laminas\\Stratigility\\": "src/" 63 | } 64 | }, 65 | "autoload-dev": { 66 | "psr-4": { 67 | "LaminasTest\\Stratigility\\": "test/" 68 | } 69 | }, 70 | "scripts": { 71 | "check": [ 72 | "@cs-check", 73 | "@test" 74 | ], 75 | "cs-check": "phpcs", 76 | "cs-fix": "phpcbf", 77 | "test": "phpunit --colors=always", 78 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", 79 | "static-analysis": "psalm --shepherd --stats" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/EmptyPipelineHandler.php: -------------------------------------------------------------------------------- 1 | className); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/EmptyPipelineException.php: -------------------------------------------------------------------------------- 1 | handle() more than once'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/MissingResponseException.php: -------------------------------------------------------------------------------- 1 | responseFactory->createResponse(StatusCode::STATUS_NOT_FOUND); 27 | 28 | $response->getBody()->write(sprintf( 29 | 'Cannot %s %s', 30 | $request->getMethod(), 31 | (string) $request->getUri() 32 | )); 33 | 34 | return $response; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/IterableMiddlewarePipeInterface.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | interface IterableMiddlewarePipeInterface extends MiddlewarePipeInterface, IteratorAggregate 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Middleware/CallableMiddlewareDecorator.php: -------------------------------------------------------------------------------- 1 | 19 | * function ( 20 | * ServerRequestInterface $request, 21 | * RequestHandlerInterface $handler 22 | * ) : ResponseInterface 23 | * 24 | * 25 | * such that it will operate as PSR-15 middleware. 26 | * 27 | * Neither the arguments nor the return value need be typehinted; however, if 28 | * the signature is incompatible, a PHP Error will likely be thrown. 29 | */ 30 | final class CallableMiddlewareDecorator implements MiddlewareInterface 31 | { 32 | /** @var callable */ 33 | private $middleware; 34 | 35 | public function __construct(callable $middleware) 36 | { 37 | $this->middleware = $middleware; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | * 43 | * @throws Exception\MissingResponseException If the decorated middleware 44 | * fails to produce a response. 45 | */ 46 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 47 | { 48 | $response = ($this->middleware)($request, $handler); 49 | if (! $response instanceof ResponseInterface) { 50 | throw Exception\MissingResponseException::forCallableMiddleware($this->middleware); 51 | } 52 | return $response; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Middleware/DoublePassMiddlewareDecorator.php: -------------------------------------------------------------------------------- 1 | 22 | * function ( 23 | * ServerRequestInterface $request, 24 | * ResponseInterface $response, 25 | * callable $next 26 | * ) : ResponseInterface 27 | * 28 | * 29 | * such that it will operate as PSR-15 middleware. 30 | * 31 | * Neither the arguments nor the return value need be typehinted; however, if 32 | * the signature is incompatible, a PHP Error will likely be thrown. 33 | */ 34 | final class DoublePassMiddlewareDecorator implements MiddlewareInterface 35 | { 36 | /** @var callable */ 37 | private $middleware; 38 | 39 | private readonly ResponseInterface $responsePrototype; 40 | 41 | /** 42 | * @throws Exception\MissingResponsePrototypeException If no response 43 | * prototype is present, and laminas-diactoros is not installed. 44 | */ 45 | public function __construct(callable $middleware, ?ResponseInterface $responsePrototype = null) 46 | { 47 | $this->middleware = $middleware; 48 | 49 | if (! $responsePrototype && ! class_exists(Response::class)) { 50 | throw Exception\MissingResponsePrototypeException::create(); 51 | } 52 | 53 | $this->responsePrototype = $responsePrototype ?? new Response(); 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | * 59 | * @throws Exception\MissingResponseException If the decorated middleware 60 | * fails to produce a response. 61 | */ 62 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 63 | { 64 | $response = ($this->middleware)( 65 | $request, 66 | $this->responsePrototype, 67 | $this->decorateHandler($handler) 68 | ); 69 | 70 | if (! $response instanceof ResponseInterface) { 71 | throw Exception\MissingResponseException::forCallableMiddleware($this->middleware); 72 | } 73 | 74 | return $response; 75 | } 76 | 77 | private function decorateHandler(RequestHandlerInterface $handler): callable 78 | { 79 | return static fn(ServerRequestInterface $request, ResponseInterface $response) => $handler->handle($request); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Middleware/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | 34 | * function ( 35 | * Throwable $e, 36 | * ServerRequestInterface $request, 37 | * ResponseInterface $response 38 | * ) : ResponseInterface 39 | * 40 | * 41 | * These are provided the error, and the request responsible; the response 42 | * provided is the response prototype provided to the ErrorHandler instance 43 | * itself, and can be used as the basis for returning an error response. 44 | * 45 | * An error response generator must be provided as a constructor argument; 46 | * if not provided, an instance of Laminas\Stratigility\Middleware\ErrorResponseGenerator 47 | * will be used. 48 | * 49 | * Listeners use the following signature: 50 | * 51 | * 52 | * function ( 53 | * Throwable $e, 54 | * ServerRequestInterface $request, 55 | * ResponseInterface $response 56 | * ) : void 57 | * 58 | * 59 | * Listeners are given the error, the request responsible, and the generated 60 | * error response, and can then react to them. They are best suited for 61 | * logging and monitoring purposes. 62 | * 63 | * Listeners are attached using the attachListener() method, and triggered 64 | * in the order attached. 65 | * 66 | * @final 67 | */ 68 | class ErrorHandler implements MiddlewareInterface 69 | { 70 | /** @var callable[] */ 71 | private array $listeners = []; 72 | 73 | /** @var callable Routine that will generate the error response. */ 74 | private $responseGenerator; 75 | 76 | /** 77 | * @param null|callable $responseGenerator Callback that will generate the final 78 | * error response; if none is provided, ErrorResponseGenerator is used. 79 | */ 80 | public function __construct( 81 | private readonly ResponseFactoryInterface $responseFactory, 82 | ?callable $responseGenerator = null 83 | ) { 84 | $this->responseGenerator = $responseGenerator ?? new ErrorResponseGenerator(); 85 | } 86 | 87 | /** 88 | * Attach an error listener. 89 | * 90 | * Each listener receives the following three arguments: 91 | * 92 | * - Throwable $error 93 | * - ServerRequestInterface $request 94 | * - ResponseInterface $response 95 | * 96 | * These instances are all immutable, and the return values of 97 | * listeners are ignored; use listeners for reporting purposes 98 | * only. 99 | */ 100 | public function attachListener(callable $listener): void 101 | { 102 | if (in_array($listener, $this->listeners, true)) { 103 | return; 104 | } 105 | 106 | $this->listeners[] = $listener; 107 | } 108 | 109 | /** 110 | * Middleware to handle errors and exceptions in layers it wraps. 111 | * 112 | * Adds an error handler that will convert PHP errors to ErrorException 113 | * instances. 114 | * 115 | * Internally, wraps the call to $next() in a try/catch block, catching 116 | * all PHP Throwables. 117 | * 118 | * When an exception is caught, an appropriate error response is created 119 | * and returned instead; otherwise, the response returned by $next is 120 | * used. 121 | */ 122 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 123 | { 124 | set_error_handler($this->createErrorHandler()); 125 | 126 | try { 127 | $response = $handler->handle($request); 128 | } catch (Throwable $e) { 129 | $response = $this->handleThrowable($e, $request); 130 | } 131 | 132 | restore_error_handler(); 133 | 134 | return $response; 135 | } 136 | 137 | /** 138 | * Handles all throwables, generating and returning a response. 139 | * 140 | * Passes the error, request, and response prototype to createErrorResponse(), 141 | * triggers all listeners with the same arguments (but using the response 142 | * returned from createErrorResponse()), and then returns the response. 143 | */ 144 | private function handleThrowable(Throwable $e, ServerRequestInterface $request): ResponseInterface 145 | { 146 | $generator = $this->responseGenerator; 147 | /** @var ResponseInterface $response */ 148 | $response = $generator($e, $request, $this->responseFactory->createResponse()); 149 | $this->triggerListeners($e, $request, $response); 150 | return $response; 151 | } 152 | 153 | /** 154 | * Creates and returns a callable error handler that raises exceptions. 155 | * 156 | * Only raises exceptions for errors that are within the error_reporting mask. 157 | * 158 | * @return callable(int, string, string=, int=, array=): bool 159 | */ 160 | private function createErrorHandler(): callable 161 | { 162 | /** 163 | * @throws ErrorException if error is not within the error_reporting mask. 164 | * @return bool|never 165 | */ 166 | return static function (int $errno, string $errstr, string $errfile = '', int $errline = 0): bool { 167 | if (! (error_reporting() & $errno)) { 168 | // error_reporting does not include this error 169 | return true; 170 | } 171 | 172 | throw new ErrorException($errstr, 0, $errno, $errfile, $errline); 173 | }; 174 | } 175 | 176 | /** 177 | * Trigger all error listeners. 178 | */ 179 | private function triggerListeners( 180 | Throwable $error, 181 | ServerRequestInterface $request, 182 | ResponseInterface $response 183 | ): void { 184 | foreach ($this->listeners as $listener) { 185 | $listener($error, $request, $response); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/Middleware/ErrorResponseGenerator.php: -------------------------------------------------------------------------------- 1 | withStatus(Utils::getStatusCode($e, $response)); 28 | $body = $response->getBody(); 29 | 30 | if ($this->isDevelopmentMode) { 31 | $escaper = new Escaper(); 32 | $body->write($escaper->escapeHtml((string) $e)); 33 | return $response; 34 | } 35 | 36 | $body->write($response->getReasonPhrase() ?: 'Unknown Error'); 37 | return $response; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Middleware/HostMiddlewareDecorator.php: -------------------------------------------------------------------------------- 1 | getUri()->getHost(); 26 | 27 | if ($host !== strtolower($this->host)) { 28 | return $handler->handle($request); 29 | } 30 | 31 | return $this->middleware->process($request, $handler); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Middleware/OriginalMessages.php: -------------------------------------------------------------------------------- 1 | withAttribute('originalUri', $request->getUri()) 34 | ->withAttribute('originalRequest', $request); 35 | 36 | return $handler->handle($request); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Middleware/PathMiddlewareDecorator.php: -------------------------------------------------------------------------------- 1 | prefix = $this->normalizePrefix($prefix); 26 | } 27 | 28 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 29 | { 30 | $path = $request->getUri()->getPath() ?: '/'; 31 | 32 | // Current path is shorter than decorator path 33 | if (strlen($path) < strlen($this->prefix)) { 34 | return $handler->handle($request); 35 | } 36 | 37 | // Current path does not match decorator path 38 | if (0 !== stripos($path, $this->prefix)) { 39 | return $handler->handle($request); 40 | } 41 | 42 | // Skip if match is not at a border ('/' or end) 43 | $border = $this->getBorder($path); 44 | if ($border && '/' !== $border) { 45 | return $handler->handle($request); 46 | } 47 | 48 | // Trim off the part of the url that matches the prefix if it is not / only 49 | $requestToProcess = $this->prefix !== '/' 50 | ? $this->prepareRequestWithTruncatedPrefix($request) 51 | : $request; 52 | 53 | // Process our middleware. 54 | // If the middleware calls on the handler, the handler should be provided 55 | // the original request, as this indicates we've left the path-segregated 56 | // layer. 57 | return $this->middleware->process( 58 | $requestToProcess, 59 | $this->prepareHandlerForOriginalRequest($handler) 60 | ); 61 | } 62 | 63 | private function getBorder(string $path): string 64 | { 65 | if ($this->prefix === '/') { 66 | return '/'; 67 | } 68 | 69 | $length = strlen($this->prefix); 70 | return strlen($path) > $length ? $path[$length] : ''; 71 | } 72 | 73 | private function prepareRequestWithTruncatedPrefix(ServerRequestInterface $request): ServerRequestInterface 74 | { 75 | $uri = $request->getUri(); 76 | $path = $this->getTruncatedPath($this->prefix, $uri->getPath()); 77 | $new = $uri->withPath($path); 78 | return $request->withUri($new); 79 | } 80 | 81 | private function getTruncatedPath(string $segment, string $path): string 82 | { 83 | if ($segment === $path) { 84 | // Decorated path and current path are the same; return empty string 85 | return ''; 86 | } 87 | 88 | // Strip decorated path from start of current path 89 | return substr($path, strlen($segment)); 90 | } 91 | 92 | private function prepareHandlerForOriginalRequest(RequestHandlerInterface $handler): RequestHandlerInterface 93 | { 94 | return new class ($handler, $this->prefix) implements RequestHandlerInterface { 95 | public function __construct( 96 | private readonly RequestHandlerInterface $handler, 97 | private readonly string $prefix 98 | ) { 99 | } 100 | 101 | /** 102 | * Invokes the composed handler with a request using the original URI. 103 | * 104 | * The decorated middleware may provide an altered response. However, 105 | * we want to reset the path to the original path on invocation, as 106 | * that is the part we originally modified, and is a part the decorated 107 | * middleware should not modify. 108 | * 109 | * {@inheritDoc} 110 | */ 111 | public function handle(ServerRequestInterface $request): ResponseInterface 112 | { 113 | $uri = $request->getUri(); 114 | $uri = $uri->withPath($this->prefix . $uri->getPath()); 115 | return $this->handler->handle($request->withUri($uri)); 116 | } 117 | }; 118 | } 119 | 120 | /** 121 | * Ensures that the right-most slash is trimmed for prefixes of more than 122 | * one character, and that the prefix begins with a slash. 123 | */ 124 | private function normalizePrefix(string $prefix): string 125 | { 126 | $prefix = strlen($prefix) > 1 ? rtrim($prefix, '/') : $prefix; 127 | if (! str_starts_with($prefix, '/')) { 128 | $prefix = '/' . $prefix; 129 | } 130 | return $prefix; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Middleware/RequestHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | handler->handle($request); 37 | } 38 | 39 | /** 40 | * Proxies to decorated handler to handle the request. 41 | */ 42 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 43 | { 44 | return $this->handler->handle($request); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/MiddlewarePipe.php: -------------------------------------------------------------------------------- 1 | */ 34 | private SplQueue $pipeline; 35 | 36 | /** 37 | * Initializes the queue. 38 | */ 39 | public function __construct() 40 | { 41 | /** @psalm-var SplQueue */ 42 | $this->pipeline = new SplQueue(); 43 | } 44 | 45 | /** 46 | * Perform a deep clone. 47 | */ 48 | public function __clone() 49 | { 50 | $this->pipeline = clone $this->pipeline; 51 | } 52 | 53 | /** 54 | * Handle an incoming request. 55 | * 56 | * Attempts to handle an incoming request by doing the following: 57 | * 58 | * - Cloning itself, to produce a request handler. 59 | * - Dequeuing the first middleware in the cloned handler. 60 | * - Processing the first middleware using the request and the cloned handler. 61 | * 62 | * If the pipeline is empty at the time this method is invoked, it will 63 | * raise an exception. 64 | * 65 | * @throws Exception\EmptyPipelineException If no middleware is present in 66 | * the instance in order to process the request. 67 | */ 68 | public function handle(ServerRequestInterface $request): ResponseInterface 69 | { 70 | return $this->process($request, new EmptyPipelineHandler(self::class)); 71 | } 72 | 73 | /** 74 | * PSR-15 middleware invocation. 75 | * 76 | * Executes the internal pipeline, passing $handler as the "final 77 | * handler" in cases when the pipeline exhausts itself. 78 | */ 79 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 80 | { 81 | return (new Next($this->pipeline, $handler))->handle($request); 82 | } 83 | 84 | /** 85 | * Attach middleware to the pipeline. 86 | */ 87 | public function pipe(MiddlewareInterface $middleware): void 88 | { 89 | $this->pipeline->enqueue($middleware); 90 | } 91 | 92 | /** @return Traversable */ 93 | public function getIterator(): Traversable 94 | { 95 | return new ArrayIterator( 96 | iterator_to_array( 97 | clone $this->pipeline, 98 | false, 99 | ), 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/MiddlewarePipeInterface.php: -------------------------------------------------------------------------------- 1 | |null */ 20 | private ?SplQueue $queue; 21 | 22 | /** 23 | * Clones the queue provided to allow re-use. 24 | * 25 | * @param SplQueue $queue 26 | * @param RequestHandlerInterface $fallbackHandler Fallback handler to 27 | * invoke when the queue is exhausted. 28 | */ 29 | public function __construct(SplQueue $queue, private RequestHandlerInterface $fallbackHandler) 30 | { 31 | $this->queue = clone $queue; 32 | } 33 | 34 | public function handle(ServerRequestInterface $request): ResponseInterface 35 | { 36 | if ($this->queue === null) { 37 | throw MiddlewarePipeNextHandlerAlreadyCalledException::create(); 38 | } 39 | 40 | if ($this->queue->isEmpty()) { 41 | $this->queue = null; 42 | return $this->fallbackHandler->handle($request); 43 | } 44 | 45 | $middleware = $this->queue->dequeue(); 46 | $next = clone $this; // deep clone is not used intentionally 47 | $this->queue = null; // mark queue as processed at this nesting level 48 | 49 | return $middleware->process($request, $next); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 | getCode(); 30 | if (is_int($errorCode) && $errorCode >= 400 && $errorCode < 600) { 31 | return $errorCode; 32 | } 33 | 34 | $status = $response->getStatusCode(); 35 | if ($status < 400 || $status >= 600) { 36 | $status = 500; 37 | } 38 | return $status; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/functions/double-pass-middleware.php: -------------------------------------------------------------------------------- 1 | 15 | * use function Laminas\Stratigility\doublePassMiddleware; 16 | * 17 | * $pipeline->pipe(doublePassMiddleware(function ($req, $res, $next) { 18 | * // do some work 19 | * })); 20 | * 21 | * 22 | * Optionally, pass a response prototype as well, if using a PSR-7 23 | * implementation other than laminas-diactoros: 24 | * 25 | * 26 | * $pipeline->pipe(doublePassMiddleware(function ($req, $res, $next) { 27 | * // do some work 28 | * }, $responsePrototype)); 29 | * 30 | */ 31 | function doublePassMiddleware( 32 | callable $middleware, 33 | ?ResponseInterface $response = null 34 | ): Middleware\DoublePassMiddlewareDecorator { 35 | return new Middleware\DoublePassMiddlewareDecorator($middleware, $response); 36 | } 37 | -------------------------------------------------------------------------------- /src/functions/host.php: -------------------------------------------------------------------------------- 1 | 15 | * use function Laminas\Stratigility\host; 16 | * 17 | * $pipeline->pipe(host('host.foo', $middleware)); 18 | * 19 | */ 20 | function host(string $host, MiddlewareInterface $middleware): Middleware\HostMiddlewareDecorator 21 | { 22 | return new Middleware\HostMiddlewareDecorator($host, $middleware); 23 | } 24 | -------------------------------------------------------------------------------- /src/functions/middleware.php: -------------------------------------------------------------------------------- 1 | 13 | * use function Laminas\Stratigility\middleware; 14 | * 15 | * $pipeline->pipe(middleware(function ($req, $handler) { 16 | * // do some work 17 | * })); 18 | * 19 | */ 20 | function middleware(callable $middleware): Middleware\CallableMiddlewareDecorator 21 | { 22 | return new Middleware\CallableMiddlewareDecorator($middleware); 23 | } 24 | -------------------------------------------------------------------------------- /src/functions/path.php: -------------------------------------------------------------------------------- 1 | 15 | * use function Laminas\Stratigility\path; 16 | * 17 | * $pipeline->pipe(path('/foo', $middleware)); 18 | * 19 | */ 20 | function path(string $path, MiddlewareInterface $middleware): Middleware\PathMiddlewareDecorator 21 | { 22 | return new Middleware\PathMiddlewareDecorator($path, $middleware); 23 | } 24 | --------------------------------------------------------------------------------