├── 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 | [](https://github.com/laminas/laminas-stratigility/actions/workflows/continuous-integration.yml)
4 | [](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 |
--------------------------------------------------------------------------------