Skip to content

Commit 0a596be

Browse files
authored
Update Pipeline.php
1 parent 140d969 commit 0a596be

File tree

1 file changed

+226
-42
lines changed

1 file changed

+226
-42
lines changed

src/Pipeline.php

Lines changed: 226 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,42 @@
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
*/
2048
final 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

Comments
 (0)