1111/**
1212 * Class Pipeline
1313 *
14- * This class is responsible for processing a server request through a chain of middleware.
15- * It implements the PipelineInterface and provides methods for:
16- * - Adding middleware via the pipe method.
17- * - Sequentially processing a request via the process method.
18- * - Handling a request with a fallback handler when the middleware chain is exhausted.
14+ * Stateless middleware pipeline that processes HTTP requests through a chain of PSR-15 middleware.
15+ * This implementation uses a recursive approach without mutable state, making it thread-safe
16+ * and safe for concurrent request processing.
17+ *
18+ * Key features:
19+ * - Thread-safe: No mutable position state
20+ * - Immutable: pipe() returns new instances
21+ * - Composable: Pipelines can contain other pipelines
22+ * - PSR-15 compliant: Implements MiddlewareInterface and RequestHandlerInterface
23+ *
24+ * @example Basic usage
25+ * ```php
26+ * $pipeline = new Pipeline([
27+ * new AuthenticationMiddleware(),
28+ * new LoggingMiddleware(),
29+ * new CorsMiddleware(),
30+ * ], new ApplicationHandler());
31+ *
32+ * $response = $pipeline->handle($request);
33+ * ```
34+ *
35+ * @example Pipeline composition
36+ * ```php
37+ * $apiPipeline = new Pipeline([
38+ * new RateLimitMiddleware(),
39+ * new JsonMiddleware(),
40+ * ]);
41+ *
42+ * $mainPipeline = new Pipeline([
43+ * new LoggingMiddleware(),
44+ * $apiPipeline, // Pipeline as middleware
45+ * ], $handler);
46+ * ```
1947 */
2048final class Pipeline implements PipelineInterface
2149{
22- /**
23- * Current position in the middleware chain.
24- *
25- * @var int
26- */
27- private int $ position = 0 ;
28-
2950 /**
3051 * An array of middleware objects that implement MiddlewareInterface.
3152 *
@@ -45,17 +66,21 @@ final class Pipeline implements PipelineInterface
4566 * Defaults to an instance of EmptyPipelineHandler.
4667 *
4768 * @throws \InvalidArgumentException if any middleware does not implement MiddlewareInterface.
48- * Error Message: "Provided middleware does not implement MiddlewareInterface"
69+ *
70+ * @example
71+ * ```php
72+ * $pipeline = new Pipeline([
73+ * new AuthMiddleware(),
74+ * new ValidationMiddleware(),
75+ * ], new AppHandler());
76+ * ```
4977 */
5078 public function __construct (
5179 iterable $ middlewares = [],
5280 private(set) RequestHandlerInterface $ fallbackHandler = new EmptyPipelineHandler
5381 ) {
5482 foreach ($ middlewares as $ i => $ middleware ) {
55- if (!$ middleware instanceof MiddlewareInterface) {
56- throw new \InvalidArgumentException ("Provided middlewares ( $ i) does not implement " . MiddlewareInterface::class);
57- }
58-
83+ $ this ->validateMiddleware ($ middleware , $ i );
5984 $ this ->middlewares [] = $ middleware ;
6085 }
6186 }
@@ -66,20 +91,50 @@ public function __construct(
6691 *
6792 * @param MiddlewareInterface|class-string<T> $middleware The middleware instance or class to search for.
6893 * @return bool Returns true if the middleware exists; false otherwise.
94+ *
95+ * @example Check by class name
96+ * ```php
97+ * if ($pipeline->has(AuthMiddleware::class)) {
98+ * // Pipeline includes authentication
99+ * }
100+ * ```
101+ *
102+ * @example Check by instance
103+ * ```php
104+ * $auth = new AuthMiddleware();
105+ * if ($pipeline->has($auth)) {
106+ * // This specific instance is in the pipeline
107+ * }
108+ * ```
69109 */
70110 public function has (MiddlewareInterface |string $ middleware ): bool
71111 {
72- if ($ middleware instanceof MiddlewareInterface) $ middleware = $ middleware ::class;
112+ foreach ($ this ->middlewares as $ m ) {
113+ // Check by instance (identity)
114+ if ($ middleware instanceof MiddlewareInterface && $ m === $ middleware ) {
115+ return true ;
116+ }
117+
118+ // Check by class
119+ if (is_string ($ middleware ) && $ m ::class === $ middleware ) {
120+ return true ;
121+ }
122+ }
73123
74- return array_any ($ this ->middlewares ,
75- static fn (MiddlewareInterface $ m ) => $ middleware === $ m ::class
76- );
124+ return false ;
77125 }
78126
79127 /**
80128 * Checks if the pipeline is empty.
81129 *
82130 * @return bool True if there are no middleware registered; false otherwise.
131+ *
132+ * @example
133+ * ```php
134+ * if ($pipeline->isEmpty()) {
135+ * // Request will go directly to fallback handler
136+ * }
137+ * ```
83138 */
84139 public function isEmpty (): bool
85140 {
@@ -92,6 +147,11 @@ public function isEmpty(): bool
92147 * Implements the Countable interface.
93148 *
94149 * @return int The count of middleware components.
150+ *
151+ * @example
152+ * ```php
153+ * echo "Pipeline has " . count($pipeline) . " middleware";
154+ * ```
95155 */
96156 public function count (): int
97157 {
@@ -104,6 +164,13 @@ public function count(): int
104164 * Implements the IteratorAggregate interface, allowing foreach iteration over the middleware.
105165 *
106166 * @return \Generator<MiddlewareInterface> A generator that yields each middleware in the pipeline.
167+ *
168+ * @example
169+ * ```php
170+ * foreach ($pipeline as $middleware) {
171+ * echo get_class($middleware) . "\n";
172+ * }
173+ * ```
107174 */
108175 public function getIterator (): \Generator
109176 {
@@ -113,12 +180,14 @@ public function getIterator(): \Generator
113180 /**
114181 * Performs a deep clone of the pipeline.
115182 *
116- * When cloning, each registered middleware is also cloned to prevent shared state issues.
183+ * When cloning, each registered middleware and the fallback handler are also cloned
184+ * to prevent shared state issues.
117185 */
118186 public function __clone ()
119187 {
120- $ this ->position = 0 ;
121- foreach ($ this ->middlewares as $ i => $ middleware ) $ this ->middlewares [$ i ] = clone $ middleware ;
188+ foreach ($ this ->middlewares as $ i => $ middleware ) {
189+ $ this ->middlewares [$ i ] = clone $ middleware ;
190+ }
122191 $ this ->fallbackHandler = clone $ this ->fallbackHandler ;
123192 }
124193
@@ -135,51 +204,153 @@ public function __clone()
135204 *
136205 * @throws \InvalidArgumentException If any provided middleware does not implement MiddlewareInterface.
137206 * @throws \RuntimeException If middleware refers to the pipeline itself.
207+ *
208+ * @example Append middleware
209+ * ```php
210+ * $pipeline = $pipeline->pipe([
211+ * new CacheMiddleware(),
212+ * new CompressionMiddleware(),
213+ * ]);
214+ * ```
215+ *
216+ * @example Prepend middleware (execute first)
217+ * ```php
218+ * $pipeline = $pipeline->pipe(new SecurityHeadersMiddleware(), prepend: true);
219+ * ```
138220 */
139221 public function pipe (iterable |MiddlewareInterface $ middlewares , bool $ prepend = false ): PipelineInterface
140222 {
141223 $ copy = clone $ this ;
142224
143- if ($ middlewares instanceof MiddlewareInterface) $ middlewares = [$ middlewares ];
225+ if ($ middlewares instanceof MiddlewareInterface) {
226+ $ middlewares = [$ middlewares ];
227+ }
144228
145229 foreach ($ middlewares as $ i => $ middleware ) {
146- if (!$ middleware instanceof MiddlewareInterface) {
147- throw new \InvalidArgumentException ("Provided middlewares ( $ i) does not implement " . MiddlewareInterface::class);
148- }
230+ $ this ->validateMiddleware ($ middleware , $ i );
149231
150232 if ($ middleware === $ this ) {
151- throw new \RuntimeException ('Middleware cannot be the pipeline itself ' );
233+ throw new \RuntimeException ('Cannot add pipeline to itself - this would create circular reference ' );
152234 }
153235
154- if ($ prepend ) array_unshift ($ copy ->middlewares , $ middleware );
155- else $ copy ->middlewares [] = $ middleware ;
236+ // Check for nested circular references
237+ if ($ middleware instanceof PipelineInterface && $ middleware ->has ($ this )) {
238+ throw new \RuntimeException ('Cannot add pipeline that contains reference to this pipeline ' );
239+ }
240+
241+ if ($ prepend ) {
242+ array_unshift ($ copy ->middlewares , $ middleware );
243+ } else {
244+ $ copy ->middlewares [] = $ middleware ;
245+ }
156246 }
157247
158248 return $ copy ;
159249 }
160250
161251 /**
162- * @inheritDoc
252+ * Processes a request through the middleware chain.
253+ *
254+ * This method implements MiddlewareInterface::process(), allowing the pipeline
255+ * to act as middleware within another pipeline.
256+ *
257+ * @param ServerRequestInterface $request The server request to process.
258+ * @param RequestHandlerInterface $handler The handler to use when middleware chain is exhausted.
259+ * @return ResponseInterface The HTTP response.
260+ *
261+ * @example Using pipeline as middleware
262+ * ```php
263+ * $innerPipeline = new Pipeline([new ValidatorMiddleware()]);
264+ *
265+ * $outerPipeline = new Pipeline([
266+ * new LoggingMiddleware(),
267+ * $innerPipeline, // Acts as middleware
268+ * ], $handler);
269+ * ```
163270 */
164271 public function process (ServerRequestInterface $ request , RequestHandlerInterface $ handler ): ResponseInterface
165272 {
166- $ middleware = $ this ->middlewares [$ this ->position ++] ?? null ;
273+ return $ this ->processMiddleware ($ request , 0 , $ handler );
274+ }
275+
276+ /**
277+ * Handles a request through the middleware chain using the fallback handler.
278+ *
279+ * This method implements RequestHandlerInterface::handle(), allowing the pipeline
280+ * to act as a request handler.
281+ *
282+ * @param ServerRequestInterface $request The server request to handle.
283+ * @return ResponseInterface The HTTP response.
284+ *
285+ * @example
286+ * ```php
287+ * $pipeline = new Pipeline($middlewares, $appHandler);
288+ * $response = $pipeline->handle($request);
289+ * ```
290+ */
291+ public function handle (ServerRequestInterface $ request ): ResponseInterface
292+ {
293+ return $ this ->processMiddleware ($ request , 0 , $ this ->fallbackHandler );
294+ }
167295
168- try {
169- if (!$ middleware ) {
170- return $ handler ->handle ($ request );
296+ /**
297+ * Recursively processes middleware starting from the given position.
298+ *
299+ * This stateless approach eliminates the need for mutable position tracking,
300+ * making the pipeline safe for concurrent use and easier to reason about.
301+ *
302+ * @param ServerRequestInterface $request The request to process.
303+ * @param int $position Current position in the middleware chain.
304+ * @param RequestHandlerInterface $finalHandler Handler to use when all middleware are exhausted.
305+ * @return ResponseInterface The HTTP response.
306+ */
307+ private function processMiddleware (
308+ ServerRequestInterface $ request ,
309+ int $ position ,
310+ RequestHandlerInterface $ finalHandler
311+ ): ResponseInterface {
312+ // If we've exhausted all middleware, use the final handler
313+ if (!isset ($ this ->middlewares [$ position ])) {
314+ return $ finalHandler ->handle ($ request );
315+ }
316+
317+ // Create a handler that will process the next middleware in the chain
318+ $ nextPosition = $ position + 1 ;
319+ $ handler = new class ($ nextPosition , $ finalHandler , function ($ req , $ pos , $ fh ) {
320+ return $ this ->processMiddleware ($ req , $ pos , $ fh );
321+ }) implements RequestHandlerInterface {
322+ public function __construct (
323+ private readonly int $ nextPosition ,
324+ private readonly RequestHandlerInterface $ finalHandler ,
325+ private readonly \Closure $ processor
326+ ) {}
327+
328+ public function handle (ServerRequestInterface $ request ): ResponseInterface
329+ {
330+ return ($ this ->processor )($ request , $ this ->nextPosition , $ this ->finalHandler );
171331 }
332+ };
172333
173- return $ middleware ->process ($ request , $ this );
174- } finally { $ this ->position = 0 ; }
334+ return $ this ->middlewares [$ position ]->process ($ request , $ handler );
175335 }
176336
177337 /**
178- * @inheritDoc
338+ * Validates that the given value is middleware.
339+ *
340+ * @param mixed $middleware The value to validate.
341+ * @param int|string $position The position of the middleware in the collection.
342+ * @return void
343+ *
344+ * @throws \InvalidArgumentException If the value does not implement MiddlewareInterface.
179345 */
180- public function handle ( ServerRequestInterface $ request ): ResponseInterface
346+ private function validateMiddleware ( mixed $ middleware , int | string $ position ): void
181347 {
182- return $ this ->process ($ request , $ this ->fallbackHandler );
348+ if (!$ middleware instanceof MiddlewareInterface) {
349+ $ type = get_debug_type ($ middleware );
350+ throw new \InvalidArgumentException (
351+ "Middleware at position $ position must implement " . MiddlewareInterface::class . ", $ type given "
352+ );
353+ }
183354 }
184355
185356 /**
@@ -189,10 +360,18 @@ public function handle(ServerRequestInterface $request): ResponseInterface
189360 * and optionally, a custom fallback handler. If no fallback handler is provided, the default
190361 * EmptyPipelineHandler is used.
191362 *
192- * @param iterable<MiddlewareInterface> $middlewares An iterable collection of middleware objects.
363+ * @param iterable<MiddlewareInterface> $middlewares An iterable collection of middleware objects.
193364 * @param ?RequestHandlerInterface $fallbackHandler The fallback handler to use.
194365 *
195366 * @return self Returns a fully configured Pipeline instance.
367+ *
368+ * @example
369+ * ```php
370+ * $pipeline = Pipeline::createFromIterable([
371+ * new AuthMiddleware(),
372+ * new ValidationMiddleware(),
373+ * ], new ApplicationHandler());
374+ * ```
196375 */
197376 public static function createFromIterable (iterable $ middlewares , ?RequestHandlerInterface $ fallbackHandler = null ): PipelineInterface
198377 {
@@ -208,6 +387,11 @@ public static function createFromIterable(iterable $middlewares, ?RequestHandler
208387 * @param RequestHandlerInterface $handler The new fallback handler to be used when the middleware chain is exhausted.
209388 *
210389 * @return PipelineInterface Returns the cloned pipeline instance with the updated fallback handler.
390+ *
391+ * @example
392+ * ```php
393+ * $pipeline = $pipeline->withFallbackHandler(new CustomHandler());
394+ * ```
211395 */
212396 public function withFallbackHandler (RequestHandlerInterface $ handler ): PipelineInterface
213397 {
0 commit comments