Skip to content

Commit e5f85c1

Browse files
authored
Update Pipeline.php
1 parent fd3ce63 commit e5f85c1

File tree

1 file changed

+31
-188
lines changed

1 file changed

+31
-188
lines changed

src/Pipeline.php

Lines changed: 31 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
<?php
23

34
namespace Bermuda\Http\Middleware;
@@ -12,8 +13,7 @@
1213
* Class Pipeline
1314
*
1415
* 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.
16+
* Uses SplQueue for efficient sequential processing without position tracking.
1717
*
1818
* Key features:
1919
* - Thread-safe: No mutable position state
@@ -31,57 +31,33 @@
3131
*
3232
* $response = $pipeline->handle($request);
3333
* ```
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-
* ```
4734
*/
4835
final class Pipeline implements PipelineInterface
4936
{
5037
/**
51-
* An array of middleware objects that implement MiddlewareInterface.
38+
* Queue of middleware objects that implement MiddlewareInterface.
5239
*
53-
* @var MiddlewareInterface[]
40+
* @var \SplQueue<MiddlewareInterface>
5441
*/
55-
private array $middlewares = [];
42+
private \SplQueue $middlewares;
5643

5744
/**
5845
* Constructor for the Pipeline class.
5946
*
60-
* Initializes the Pipeline with an optional iterable of middleware and a fallback handler.
61-
* Each middleware in the provided iterable is verified to implement MiddlewareInterface.
62-
* If any middleware fails this verification, an InvalidArgumentException is thrown.
63-
*
6447
* @param iterable<MiddlewareInterface> $middlewares An iterable collection of middleware objects.
6548
* @param RequestHandlerInterface $fallbackHandler The fallback handler to use when the middleware chain is exhausted.
66-
* Defaults to an instance of EmptyPipelineHandler.
6749
*
6850
* @throws \InvalidArgumentException if any middleware does not implement MiddlewareInterface.
69-
*
70-
* @example
71-
* ```php
72-
* $pipeline = new Pipeline([
73-
* new AuthMiddleware(),
74-
* new ValidationMiddleware(),
75-
* ], new AppHandler());
76-
* ```
7751
*/
7852
public function __construct(
7953
iterable $middlewares = [],
8054
private(set) RequestHandlerInterface $fallbackHandler = new EmptyPipelineHandler
8155
) {
56+
$this->middlewares = new \SplQueue();
57+
8258
foreach ($middlewares as $i => $middleware) {
8359
$this->validateMiddleware($middleware, $i);
84-
$this->middlewares[] = $middleware;
60+
$this->middlewares->enqueue($middleware);
8561
}
8662
}
8763

@@ -91,21 +67,6 @@ public function __construct(
9167
*
9268
* @param MiddlewareInterface|class-string<T> $middleware The middleware instance or class to search for.
9369
* @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-
* ```
10970
*/
11071
public function has(MiddlewareInterface|string $middleware): bool
11172
{
@@ -128,95 +89,56 @@ public function has(MiddlewareInterface|string $middleware): bool
12889
* Checks if the pipeline is empty.
12990
*
13091
* @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-
* ```
13892
*/
13993
public function isEmpty(): bool
14094
{
141-
return empty($this->middlewares);
95+
return $this->middlewares->isEmpty();
14296
}
14397

14498
/**
14599
* Returns the number of middleware objects in the pipeline.
146100
*
147-
* Implements the Countable interface.
148-
*
149101
* @return int The count of middleware components.
150-
*
151-
* @example
152-
* ```php
153-
* echo "Pipeline has " . count($pipeline) . " middleware";
154-
* ```
155102
*/
156103
public function count(): int
157104
{
158-
return count($this->middlewares);
105+
return $this->middlewares->count();
159106
}
160107

161108
/**
162109
* Retrieves an external iterator over the middleware collection.
163110
*
164-
* Implements the IteratorAggregate interface, allowing foreach iteration over the middleware.
165-
*
166111
* @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-
* ```
174112
*/
175113
public function getIterator(): \Generator
176114
{
177-
yield from $this->middlewares;
115+
foreach ($this->middlewares as $middleware) {
116+
yield $middleware;
117+
}
178118
}
179119

180120
/**
181-
* Performs a deep clone of the pipeline.
121+
* Performs a clone of the pipeline.
182122
*
183-
* When cloning, each registered middleware and the fallback handler are also cloned
184-
* to prevent shared state issues.
123+
* Clones the queue structure and the fallback handler.
124+
* Middleware themselves are not cloned as they should be stateless.
185125
*/
186-
public function __clone()
126+
public function __clone(): void
187127
{
188-
foreach ($this->middlewares as $i => $middleware) {
189-
$this->middlewares[$i] = clone $middleware;
190-
}
128+
$this->middlewares = clone $this->middlewares;
191129
$this->fallbackHandler = clone $this->fallbackHandler;
192130
}
193131

194132
/**
195133
* Adds additional middleware to the pipeline.
196134
*
197-
* This method appends (or prepends, based on the $prepend flag) the specified middleware to the pipeline.
198-
* It returns a new Pipeline instance with the middleware integrated, leaving the original instance unchanged.
199-
*
200135
* @param iterable|MiddlewareInterface $middlewares One or more middleware to add.
201136
* @param bool $prepend If true, adds the middleware at the beginning of the chain.
202137
*
203138
* @return PipelineInterface A new Pipeline instance with the added middleware.
204139
*
205140
* @throws \InvalidArgumentException If any provided middleware does not implement MiddlewareInterface.
206141
* @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-
* ```
220142
*/
221143
public function pipe(iterable|MiddlewareInterface $middlewares, bool $prepend = false): PipelineInterface
222144
{
@@ -227,22 +149,18 @@ public function pipe(iterable|MiddlewareInterface $middlewares, bool $prepend =
227149
}
228150

229151
foreach ($middlewares as $i => $middleware) {
230-
$this->validateMiddleware($middleware, $i);
152+
$this->isMiddlewareInstance($middleware, $i);
231153

232154
if ($middleware === $this) {
233155
throw new \RuntimeException('Cannot add pipeline to itself - this would create circular reference');
234156
}
235157

236-
// Check for nested circular references
237158
if ($middleware instanceof PipelineInterface && $middleware->has($this)) {
238159
throw new \RuntimeException('Cannot add pipeline that contains reference to this pipeline');
239160
}
240161

241-
if ($prepend) {
242-
array_unshift($copy->middlewares, $middleware);
243-
} else {
244-
$copy->middlewares[] = $middleware;
245-
}
162+
$prepend ? $copy->middlewares->unshift($middleware) :
163+
$copy->middlewares->enqueue($middleware);
246164
}
247165

248166
return $copy;
@@ -251,87 +169,32 @@ public function pipe(iterable|MiddlewareInterface $middlewares, bool $prepend =
251169
/**
252170
* Processes a request through the middleware chain.
253171
*
254-
* This method implements MiddlewareInterface::process(), allowing the pipeline
255-
* to act as middleware within another pipeline.
256-
*
257172
* @param ServerRequestInterface $request The server request to process.
258173
* @param RequestHandlerInterface $handler The handler to use when middleware chain is exhausted.
259174
* @return ResponseInterface The HTTP response.
260175
*
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-
* ```
176+
* @throws \RuntimeException If handler is the pipeline itself (would cause double execution).
270177
*/
271178
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
272179
{
273-
return $this->processMiddleware($request, 0, $handler);
180+
if ($handler === $this) {
181+
throw new \RuntimeException('Cannot use pipeline as its own handler - this would cause all middleware to execute twice');
182+
}
183+
184+
$next = new Next($this->middlewares, $handler);
185+
return $next->handle($request);
274186
}
275187

276188
/**
277189
* Handles a request through the middleware chain using the fallback handler.
278190
*
279-
* This method implements RequestHandlerInterface::handle(), allowing the pipeline
280-
* to act as a request handler.
281-
*
282191
* @param ServerRequestInterface $request The server request to handle.
283192
* @return ResponseInterface The HTTP response.
284-
*
285-
* @example
286-
* ```php
287-
* $pipeline = new Pipeline($middlewares, $appHandler);
288-
* $response = $pipeline->handle($request);
289-
* ```
290193
*/
291194
public function handle(ServerRequestInterface $request): ResponseInterface
292195
{
293-
return $this->processMiddleware($request, 0, $this->fallbackHandler);
294-
}
295-
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);
331-
}
332-
};
333-
334-
return $this->middlewares[$position]->process($request, $handler);
196+
$next = new Next($this->middlewares, $this->fallbackHandler);
197+
return $next->handle($request);
335198
}
336199

337200
/**
@@ -343,7 +206,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
343206
*
344207
* @throws \InvalidArgumentException If the value does not implement MiddlewareInterface.
345208
*/
346-
private function validateMiddleware(mixed $middleware, int|string $position): void
209+
private function isMiddlewareInstance(mixed $middleware, int|string $position): void
347210
{
348211
if (!$middleware instanceof MiddlewareInterface) {
349212
$type = get_debug_type($middleware);
@@ -356,22 +219,10 @@ private function validateMiddleware(mixed $middleware, int|string $position): vo
356219
/**
357220
* Creates a new Pipeline instance from a set of middleware provided as an iterable.
358221
*
359-
* This static method allows the initialization of a Pipeline with a collection of middleware,
360-
* and optionally, a custom fallback handler. If no fallback handler is provided, the default
361-
* EmptyPipelineHandler is used.
362-
*
363222
* @param iterable<MiddlewareInterface> $middlewares An iterable collection of middleware objects.
364223
* @param ?RequestHandlerInterface $fallbackHandler The fallback handler to use.
365224
*
366225
* @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-
* ```
375226
*/
376227
public static function createFromIterable(iterable $middlewares, ?RequestHandlerInterface $fallbackHandler = null): PipelineInterface
377228
{
@@ -381,17 +232,9 @@ public static function createFromIterable(iterable $middlewares, ?RequestHandler
381232
/**
382233
* Returns a new Pipeline instance with the updated fallback handler.
383234
*
384-
* This method creates a clone of the current pipeline and replaces its fallback handler with the provided handler.
385-
* By cloning the existing pipeline, it ensures that the original pipeline remains unmodified, promoting immutability.
386-
*
387235
* @param RequestHandlerInterface $handler The new fallback handler to be used when the middleware chain is exhausted.
388236
*
389237
* @return PipelineInterface Returns the cloned pipeline instance with the updated fallback handler.
390-
*
391-
* @example
392-
* ```php
393-
* $pipeline = $pipeline->withFallbackHandler(new CustomHandler());
394-
* ```
395238
*/
396239
public function withFallbackHandler(RequestHandlerInterface $handler): PipelineInterface
397240
{

0 commit comments

Comments
 (0)