Skip to content

Commit 0623cd7

Browse files
authored
Move middleware stack execution from router (#2)
1 parent ecf1a87 commit 0623cd7

16 files changed

+593
-1
lines changed
File renamed without changes.

composer.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717
"minimum-stability": "dev",
1818
"prefer-stable": true,
1919
"require": {
20-
"php": "^7.4|^8.0"
20+
"php": "^7.4|^8.0",
21+
"psr/http-message": "^1.0",
22+
"psr/http-server-handler": "^1.0",
23+
"psr/http-server-middleware": "^1.0",
24+
"yiisoft/injector": "^1.0"
2125
},
2226
"require-dev": {
2327
"infection/infection": "^0.16.3",
2428
"phpunit/phpunit": "^9.3",
29+
"nyholm/psr7": "^1.0",
2530
"vimeo/psalm": "^3.15"
2631
},
2732
"autoload": {
@@ -37,6 +42,9 @@
3742
"extra": {
3843
"branch-alias": {
3944
"dev-master": "1.0.x-dev"
45+
},
46+
"config-plugin": {
47+
"web": "config/web.php"
4048
}
4149
},
4250
"config": {

config/web.php

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
use Yiisoft\Middleware\Dispatcher\MiddlewareStack;
4+
use Yiisoft\Middleware\Dispatcher\MiddlewareStackInterface;
5+
use Yiisoft\Middleware\Dispatcher\MiddlewareFactoryInterface;
6+
use Yiisoft\Middleware\Dispatcher\MiddlewareFactory;
7+
8+
return [
9+
MiddlewareStackInterface::class => MiddlewareStack::class,
10+
MiddlewareFactoryInterface::class => MiddlewareFactory::class,
11+
];

src/.gitkeep

Whitespace-only changes.

src/MiddlewareDispatcher.php

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Middleware\Dispatcher;
6+
7+
use Psr\Http\Message\ResponseInterface;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
use Psr\Http\Server\RequestHandlerInterface;
10+
11+
final class MiddlewareDispatcher
12+
{
13+
/**
14+
* Contains a stack of middleware handler.
15+
* @var MiddlewareStackInterface stack of middleware
16+
*/
17+
private MiddlewareStackInterface $stack;
18+
19+
private MiddlewareFactoryInterface $middlewareFactory;
20+
21+
/**
22+
* @var callable[]|string[]|array[]
23+
*/
24+
private array $middlewareDefinitions = [];
25+
26+
public function __construct(MiddlewareFactoryInterface $middlewareFactory, MiddlewareStackInterface $stack)
27+
{
28+
$this->middlewareFactory = $middlewareFactory;
29+
$this->stack = $stack;
30+
}
31+
32+
public function dispatch(ServerRequestInterface $request, RequestHandlerInterface $fallbackHandler): ResponseInterface
33+
{
34+
if ($this->stack->isEmpty()) {
35+
$this->stack = $this->stack->build($this->buildMiddlewares(), $fallbackHandler);
36+
}
37+
38+
return $this->stack->handle($request);
39+
}
40+
41+
public function withMiddlewares(array $middlewareDefinitions): MiddlewareDispatcher
42+
{
43+
$clone = clone $this;
44+
$clone->middlewareDefinitions = $middlewareDefinitions;
45+
$clone->stack->reset();
46+
47+
return $clone;
48+
}
49+
50+
public function hasMiddlewares(): bool
51+
{
52+
return $this->middlewareDefinitions !== [];
53+
}
54+
55+
private function buildMiddlewares(): array
56+
{
57+
$middlewares = [];
58+
foreach ($this->middlewareDefinitions as $middlewareDefinition) {
59+
$middlewares[] = $this->middlewareFactory->create($middlewareDefinition);
60+
}
61+
62+
return $middlewares;
63+
}
64+
}

src/MiddlewareFactory.php

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Middleware\Dispatcher;
6+
7+
use InvalidArgumentException;
8+
use Psr\Container\ContainerInterface;
9+
use Psr\Http\Message\ResponseInterface;
10+
use Psr\Http\Message\ServerRequestInterface;
11+
use Psr\Http\Server\MiddlewareInterface;
12+
use Psr\Http\Server\RequestHandlerInterface;
13+
use Yiisoft\Injector\Injector;
14+
15+
final class MiddlewareFactory implements MiddlewareFactoryInterface
16+
{
17+
private ContainerInterface $container;
18+
19+
public function __construct(ContainerInterface $container)
20+
{
21+
$this->container = $container;
22+
}
23+
24+
public function create($middlewareDefinition): MiddlewareInterface
25+
{
26+
return $this->createMiddleware($middlewareDefinition);
27+
}
28+
29+
/**
30+
* @param callable|string|array $middlewareDefinition
31+
* @return MiddlewareInterface
32+
*/
33+
private function createMiddleware($middlewareDefinition): MiddlewareInterface
34+
{
35+
$this->validateMiddleware($middlewareDefinition);
36+
37+
if (is_string($middlewareDefinition)) {
38+
return $this->container->get($middlewareDefinition);
39+
}
40+
41+
return $this->wrapCallable($middlewareDefinition);
42+
}
43+
44+
private function wrapCallable($callback): MiddlewareInterface
45+
{
46+
if (is_array($callback) && !is_object($callback[0])) {
47+
[$controller, $action] = $callback;
48+
return new class($controller, $action, $this->container) implements MiddlewareInterface {
49+
private string $class;
50+
private string $method;
51+
private ContainerInterface $container;
52+
53+
public function __construct(string $class, string $method, ContainerInterface $container)
54+
{
55+
$this->class = $class;
56+
$this->method = $method;
57+
$this->container = $container;
58+
}
59+
60+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
61+
{
62+
$controller = $this->container->get($this->class);
63+
return (new Injector($this->container))->invoke([$controller, $this->method], [$request, $handler]);
64+
}
65+
};
66+
}
67+
68+
return new class($callback, $this->container) implements MiddlewareInterface {
69+
private ContainerInterface $container;
70+
private $callback;
71+
72+
public function __construct(callable $callback, ContainerInterface $container)
73+
{
74+
$this->callback = $callback;
75+
$this->container = $container;
76+
}
77+
78+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
79+
{
80+
$response = (new Injector($this->container))->invoke($this->callback, [$request, $handler]);
81+
return $response instanceof MiddlewareInterface ? $response->process($request, $handler) : $response;
82+
}
83+
};
84+
}
85+
86+
/**
87+
* @param callable|string|array $middlewareDefinition
88+
*/
89+
private function validateMiddleware($middlewareDefinition): void
90+
{
91+
if (is_string($middlewareDefinition) && is_subclass_of($middlewareDefinition, MiddlewareInterface::class)) {
92+
return;
93+
}
94+
95+
if ($this->isCallable($middlewareDefinition) && (!is_array($middlewareDefinition) || !is_object($middlewareDefinition[0]))) {
96+
return;
97+
}
98+
99+
throw new InvalidArgumentException('Parameter should be either PSR middleware class name or a callable.');
100+
}
101+
102+
private function isCallable($definition): bool
103+
{
104+
if (is_callable($definition)) {
105+
return is_object($definition)
106+
? !$definition instanceof MiddlewareInterface
107+
: true;
108+
}
109+
110+
return is_array($definition)
111+
&& array_keys($definition) === [0, 1]
112+
&& in_array(
113+
$definition[1],
114+
class_exists($definition[0]) ? get_class_methods($definition[0]) : [],
115+
true
116+
);
117+
}
118+
}

src/MiddlewareFactoryInterface.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Middleware\Dispatcher;
6+
7+
use Psr\Http\Server\MiddlewareInterface;
8+
9+
interface MiddlewareFactoryInterface
10+
{
11+
public function create($middlewareDefinition): MiddlewareInterface;
12+
}

src/MiddlewareStack.php

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Middleware\Dispatcher;
6+
7+
use Psr\Http\Message\ResponseInterface;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
use Psr\Http\Server\MiddlewareInterface;
10+
use Psr\Http\Server\RequestHandlerInterface;
11+
12+
final class MiddlewareStack implements MiddlewareStackInterface
13+
{
14+
/**
15+
* Contains a stack of middleware wrapped in handlers.
16+
* Each handler points to the handler of middleware that will be processed next.
17+
* @var RequestHandlerInterface|null stack of middleware
18+
*/
19+
private ?RequestHandlerInterface $stack = null;
20+
21+
public function build(array $middlewares, RequestHandlerInterface $fallbackHandler): MiddlewareStackInterface
22+
{
23+
$handler = $fallbackHandler;
24+
foreach ($middlewares as $middleware) {
25+
$handler = $this->wrap($middleware, $handler);
26+
}
27+
28+
$new = clone $this;
29+
$new->stack = $handler;
30+
31+
return $new;
32+
}
33+
34+
public function handle(ServerRequestInterface $request): ResponseInterface
35+
{
36+
if ($this->isEmpty()) {
37+
throw new \RuntimeException('Stack is empty.');
38+
}
39+
40+
return $this->stack->handle($request);
41+
}
42+
43+
public function reset(): void
44+
{
45+
$this->stack = null;
46+
}
47+
48+
public function isEmpty(): bool
49+
{
50+
return $this->stack === null;
51+
}
52+
53+
/**
54+
* Wraps handler by middlewares
55+
*/
56+
private function wrap(MiddlewareInterface $middleware, RequestHandlerInterface $handler): RequestHandlerInterface
57+
{
58+
return new class($middleware, $handler) implements RequestHandlerInterface {
59+
private MiddlewareInterface $middleware;
60+
private RequestHandlerInterface $handler;
61+
62+
public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $handler)
63+
{
64+
$this->middleware = $middleware;
65+
$this->handler = $handler;
66+
}
67+
68+
public function handle(ServerRequestInterface $request): ResponseInterface
69+
{
70+
return $this->middleware->process($request, $this->handler);
71+
}
72+
};
73+
}
74+
}

src/MiddlewareStackInterface.php

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Middleware\Dispatcher;
6+
7+
use Psr\Http\Server\RequestHandlerInterface;
8+
9+
interface MiddlewareStackInterface extends RequestHandlerInterface
10+
{
11+
public function build(array $middlewares, RequestHandlerInterface $fallbackHandler): MiddlewareStackInterface;
12+
13+
public function reset(): void;
14+
15+
public function isEmpty(): bool;
16+
}

tests/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)