Skip to content

Commit 3cb6b7b

Browse files
committed
Add withOrder to PipelineRunner
1 parent ab940c9 commit 3cb6b7b

File tree

4 files changed

+112
-29
lines changed

4 files changed

+112
-29
lines changed

Slim/Routing/PipelineOrder.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/**
4+
* Slim Framework (https://slimframework.com)
5+
*
6+
* @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Slim\Routing;
12+
13+
enum PipelineOrder: int
14+
{
15+
case FIFO = 1;
16+
case LIFO = 2;
17+
}

Slim/Routing/PipelineRunner.php

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,9 @@
1515
use Psr\Http\Server\MiddlewareInterface;
1616
use Psr\Http\Server\RequestHandlerInterface;
1717
use RuntimeException;
18-
1918
use Slim\Interfaces\ContainerResolverInterface;
2019

21-
use function current;
2220
use function is_callable;
23-
use function next;
2421
use function sprintf;
2522

2623
/**
@@ -30,14 +27,17 @@ final class PipelineRunner implements RequestHandlerInterface
3027
{
3128
private ContainerResolverInterface $resolver;
3229

30+
private PipelineOrder $order;
31+
3332
/**
34-
* @var array<MiddlewareInterface|RequestHandlerInterface|callable|string>
33+
* @var array<int, mixed>
3534
*/
3635
private array $pipeline = [];
3736

38-
public function __construct(ContainerResolverInterface $resolver)
37+
public function __construct(ContainerResolverInterface $resolver, PipelineOrder $order = PipelineOrder::FIFO)
3938
{
4039
$this->resolver = $resolver;
40+
$this->order = $order;
4141
}
4242

4343
/**
@@ -46,40 +46,49 @@ public function __construct(ContainerResolverInterface $resolver)
4646
public function withPipeline(array $pipeline): self
4747
{
4848
$clone = clone $this;
49-
$clone->pipeline = $pipeline;
49+
$clone->pipeline = array_values($pipeline);
50+
51+
return $clone;
52+
}
53+
54+
public function withOrder(PipelineOrder $order): self
55+
{
56+
$clone = clone $this;
57+
$clone->order = $order;
5058

5159
return $clone;
5260
}
5361

5462
public function handle(ServerRequestInterface $request): ResponseInterface
5563
{
56-
$middleware = current($this->pipeline);
64+
$entry = $this->order === PipelineOrder::FIFO
65+
? array_shift($this->pipeline)
66+
: array_pop($this->pipeline);
5767

58-
if (!$middleware) {
59-
throw new RuntimeException('No middleware found. Add a response factory middleware.');
68+
if (!$entry) {
69+
throw new RuntimeException('The middleware pipeline is empty.');
6070
}
6171

62-
$middleware = $this->resolver->resolve($middleware);
63-
64-
next($this->pipeline);
72+
$entry = $this->resolver->resolve($entry);
6573

66-
if ($middleware instanceof MiddlewareInterface) {
67-
return $middleware->process($request, $this);
74+
if ($entry instanceof MiddlewareInterface) {
75+
return $entry->process($request, $this);
6876
}
6977

70-
if ($middleware instanceof RequestHandlerInterface) {
71-
return $middleware->handle($request);
78+
if ($entry instanceof RequestHandlerInterface) {
79+
return $entry->handle($request);
7280
}
7381

74-
if (is_callable($middleware)) {
75-
return $middleware($request, $this);
82+
if (is_callable($entry)) {
83+
return $entry($request, $this);
7684
}
7785

7886
throw new RuntimeException(
7987
sprintf(
80-
'Invalid middleware queue entry "%s". Middleware must either be callable or implement %s.',
81-
is_scalar($middleware) ? (string)$middleware : gettype($middleware),
88+
'Invalid pipeline entry of type "%s". Expected one of: callable, %s, or %s.',
89+
is_object($entry) ? $entry::class : gettype($entry),
8290
MiddlewareInterface::class,
91+
RequestHandlerInterface::class,
8392
),
8493
);
8594
}

tests/RequestHandler/MiddlewareRequestHandlerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function ($req, $handler) {
5353
public function testHandleWithoutMiddlewareStack()
5454
{
5555
$this->expectException(RuntimeException::class);
56-
$this->expectExceptionMessage('No middleware found. Add a response factory middleware.');
56+
$this->expectExceptionMessage('The middleware pipeline is empty.');
5757

5858
$app = AppFactory::create();
5959

tests/RequestHandler/RunnerTest.php

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
use Psr\Http\Server\RequestHandlerInterface;
1515
use RuntimeException;
1616
use Slim\Factory\AppFactory;
17+
use Slim\Routing\PipelineOrder;
1718
use Slim\Routing\PipelineRunner;
1819
use stdClass;
1920

2021
final class RunnerTest extends TestCase
2122
{
22-
public function testHandleWithMiddlewareInterface()
23+
public function testHandleWithMiddlewareInterface(): void
2324
{
2425
$app = AppFactory::create();
2526

@@ -61,7 +62,7 @@ function () use ($response) {
6162
$this->assertSame('Success', $result->getHeaderLine('X-Result'));
6263
}
6364

64-
public function testHandleWithRequestHandlerInterface()
65+
public function testHandleWithRequestHandlerInterface(): void
6566
{
6667
$app = AppFactory::create();
6768

@@ -99,7 +100,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
99100
$this->assertSame('Handled', $result->getHeaderLine('X-Handler'));
100101
}
101102

102-
public function testHandleWithCallableMiddleware()
103+
public function testHandleWithCallableMiddleware(): void
103104
{
104105
$app = AppFactory::create();
105106

@@ -127,10 +128,10 @@ function (ServerRequestInterface $req, RequestHandlerInterface $handler) use ($r
127128
$this->assertSame('Called', $result->getHeaderLine('X-Callable'));
128129
}
129130

130-
public function testHandleWithEmptyQueueThrowsException()
131+
public function testHandleWithEmptyQueueThrowsException(): void
131132
{
132133
$this->expectException(RuntimeException::class);
133-
$this->expectExceptionMessage('No middleware found. Add a response factory middleware.');
134+
$this->expectExceptionMessage('The middleware pipeline is empty.');
134135

135136
$app = AppFactory::create();
136137

@@ -149,10 +150,12 @@ public function testHandleWithEmptyQueueThrowsException()
149150
$runner->handle($request);
150151
}
151152

152-
public function testHandleWithInvalidObjectMiddlewareThrowsException()
153+
public function testHandleWithInvalidObjectMiddlewareThrowsException(): void
153154
{
154155
$this->expectException(RuntimeException::class);
155-
$this->expectExceptionMessage('Invalid middleware queue entry "object"');
156+
$this->expectExceptionMessage(
157+
'Invalid pipeline entry of type "stdClass". Expected one of: callable, Psr\Http\Server\MiddlewareInterface, or Psr\Http\Server\RequestHandlerInterface.',
158+
);
156159

157160
$app = AppFactory::create();
158161

@@ -169,7 +172,7 @@ public function testHandleWithInvalidObjectMiddlewareThrowsException()
169172
$runner->handle($request);
170173
}
171174

172-
public function testHandleWithInvalidMiddlewareStringThrowsException()
175+
public function testHandleWithInvalidMiddlewareStringThrowsException(): void
173176
{
174177
$this->expectException(NotFoundException::class);
175178
$this->expectExceptionMessage("No entry or class found for 'foo'");
@@ -188,4 +191,58 @@ public function testHandleWithInvalidMiddlewareStringThrowsException()
188191

189192
$runner->handle($request);
190193
}
194+
195+
public function testHandleExecutesPipelineInLifoOrder(): void
196+
{
197+
$app = AppFactory::create();
198+
$container = $app->getContainer();
199+
200+
$request = $container
201+
->get(ServerRequestFactoryInterface::class)
202+
->createServerRequest('GET', '/');
203+
204+
$responseFactory = $container->get(ResponseFactoryInterface::class);
205+
206+
// This middleware will be executed LAST in LIFO mode (because it was added first).
207+
$first = new class implements MiddlewareInterface {
208+
public function process(
209+
ServerRequestInterface $request,
210+
RequestHandlerInterface $handler,
211+
): ResponseInterface {
212+
$response = $handler->handle($request);
213+
return $response->withHeader('X-Order', 'First');
214+
}
215+
};
216+
217+
// This middleware will be executed FIRST in LIFO mode.
218+
$second = new class implements MiddlewareInterface {
219+
public function process(
220+
ServerRequestInterface $request,
221+
RequestHandlerInterface $handler,
222+
): ResponseInterface {
223+
$response = $handler->handle($request);
224+
return $response->withHeader('X-Order', 'Second');
225+
}
226+
};
227+
228+
// Final handler that produces a basic response
229+
$finalHandler = fn() => $responseFactory->createResponse();
230+
231+
$runner = $container
232+
->get(PipelineRunner::class)
233+
->withOrder(PipelineOrder::LIFO)
234+
->withPipeline(
235+
[
236+
$finalHandler, // end
237+
$second, // ^ second
238+
$first, // ^ start
239+
],
240+
);
241+
242+
$response = $runner->handle($request);
243+
244+
$this->assertSame('First', $response->getHeaderLine('X-Order'));
245+
}
246+
247+
191248
}

0 commit comments

Comments
 (0)