Passed
Push — master ( b5f106...996d29 )
by Rustam
02:23
created

php$1 ➔ resolveHandlerArguments()   B

Complexity

Conditions 9

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 9

Importance

Changes 5
Bugs 4 Features 0
Metric Value
c 5
b 4
f 0
dl 0
loc 30
ccs 17
cts 17
cp 1
rs 8.0555
cc 9
crap 9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Middleware\Dispatcher;
6
7
use Closure;
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 ReflectionNamedType;
14
use Yiisoft\Injector\Injector;
15
16
use function in_array;
17
use function is_array;
18
use function is_string;
19
20
/**
21
 * Creates a PSR-15 middleware based on the definition provided.
22
 */
23
final class MiddlewareFactory implements MiddlewareFactoryInterface
24
{
25
    private ContainerInterface $container;
26
27
    /**
28
     * @param ContainerInterface $container Container to use for resolving definitions.
29
     */
30 28
    public function __construct(ContainerInterface $container)
31
    {
32 28
        $this->container = $container;
33 28
    }
34
35
    /**
36
     * @param array|callable|string $middlewareDefinition Middleware definition in one of the following formats:
37
     *
38
     * - A name of PSR-15 middleware class. The middleware instance will be obtained from container and executed.
39
     * - A callable with `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface`
40
     *   signature.
41
     * - A controller handler action in format `[TestController::class, 'index']`. `TestController` instance will
42
     *   be created and `index()` method will be executed.
43
     * - A function returning a middleware. The middleware returned will be executed.
44
     *
45
     * For handler action and callable
46
     * typed parameters are automatically injected using dependency injection container.
47
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
48
     * and {@see RequestHandlerInterface}.
49
     */
50 25
    public function create($middlewareDefinition): MiddlewareInterface
51
    {
52 25
        $this->validateMiddleware($middlewareDefinition);
53
54 17
        if (is_string($middlewareDefinition)) {
55
            /** @var MiddlewareInterface */
56 3
            return $this->container->get($middlewareDefinition);
57
        }
58
59 15
        return $this->wrapCallable($middlewareDefinition);
60
    }
61
62
    /**
63
     * @param array|callable $callback
64
     */
65 15
    private function wrapCallable($callback): MiddlewareInterface
66
    {
67 15
        if (is_array($callback)) {
68 6
            return new class ($this->container, $callback) implements MiddlewareInterface {
69
                /**
70
                 * @psalm-var class-string
71
                 */
72
                private string $class;
73
                private string $method;
74
                private ContainerInterface $container;
75
                private array $callback;
76
77
                public function __construct(ContainerInterface $container, array $callback)
78
                {
79 6
                    [$this->class, $this->method] = $callback;
80 6
                    $this->container = $container;
81 6
                    $this->callback = $callback;
82 6
                }
83
84
                public function process(
85
                    ServerRequestInterface $request,
86
                    RequestHandlerInterface $handler
87
                ): ResponseInterface {
88
                    /** @var mixed $controller */
89 6
                    $controller = $this->container->get($this->class);
90
91
                    /** @var mixed $response */
92 6
                    $response = (new Injector($this->container))
93 6
                        ->invoke(
94 6
                            [$controller, $this->method],
95 6
                            MiddlewareFactory::resolveHandlerArguments($this->callback, $request, $handler)
96
                        );
97 6
                    if ($response instanceof ResponseInterface) {
98 5
                        return $response;
99
                    }
100
101 1
                    throw new InvalidMiddlewareDefinitionException($this->callback);
102
                }
103
            };
104
        }
105
106
        /** @var callable|Closure $callback */
107
108 9
        return new class ($callback, $this->container) implements MiddlewareInterface {
109
            private ContainerInterface $container;
110
            private $callback;
111
112
            public function __construct(callable $callback, ContainerInterface $container)
113
            {
114 9
                $this->callback = $callback;
115 9
                $this->container = $container;
116 9
            }
117
118
            public function process(
119
                ServerRequestInterface $request,
120
                RequestHandlerInterface $handler
121
            ): ResponseInterface {
122
                /** @var mixed $response */
123 9
                $response = (new Injector($this->container))->invoke(
124 9
                    $this->callback,
125 9
                    MiddlewareFactory::resolveHandlerArguments($this->callback, $request, $handler)
126
                );
127 9
                if ($response instanceof ResponseInterface) {
128 6
                    return $response;
129
                }
130 3
                if ($response instanceof MiddlewareInterface) {
131 2
                    return $response->process($request, $handler);
132
                }
133 1
                throw new InvalidMiddlewareDefinitionException($this->callback);
134
            }
135
        };
136
    }
137
138
    /**
139
     * @param array|callable|string $middlewareDefinition A name of PSR-15 middleware, a callable with
140
     * `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface` signature or
141
     * a handler action (an array of [handlerClass, handlerMethod]). For handler action and callable typed parameters
142
     * are automatically injected using dependency injection container passed to the route.
143
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
144
     * and {@see RequestHandlerInterface}.
145
     *
146
     * @throws InvalidMiddlewareDefinitionException
147
     */
148 25
    private function validateMiddleware($middlewareDefinition): void
149
    {
150 25
        if (is_string($middlewareDefinition) && is_subclass_of($middlewareDefinition, MiddlewareInterface::class)) {
151 3
            return;
152
        }
153
154 23
        if ($this->isCallable($middlewareDefinition)) {
155 15
            return;
156
        }
157
158 8
        throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
159
    }
160
161
    /**
162
     * @param mixed $definition
163
     */
164 23
    private function isCallable($definition): bool
165
    {
166 23
        if ($definition instanceof Closure) {
167 9
            return true;
168
        }
169
170 14
        return is_array($definition)
171 14
            && array_keys($definition) === [0, 1]
172 14
            && is_string($definition[0])
173 14
            && is_string($definition[1])
174 7
            && in_array(
175 7
                $definition[1],
176 7
                class_exists($definition[0]) ? get_class_methods($definition[0]) : [],
177 14
                true
178
            );
179
    }
180
181
    /**
182
     * @param callable|Closure $callback
183
     * @param ServerRequestInterface $request
184
     * @param RequestHandlerInterface $handler
185
     *
186
     * @throws \ReflectionException
187
     *
188
     * @return array
189
     *
190
     * @internal
191
     */
192 15
    public static function resolveHandlerArguments(
193
        $callback,
194
        ServerRequestInterface $request,
195
        RequestHandlerInterface $handler
196
    ): array {
197 15
        if (is_array($callback)) {
198 6
            [$class, $method] = $callback;
199 6
            $parameters = (new \ReflectionMethod($class, $method))->getParameters();
200
        } else {
201 9
            $parameters = (new \ReflectionFunction(Closure::fromCallable($callback)))->getParameters();
202
        }
203
        /** @psalm-var array{array-key, mixed} $arguments */
204 15
        $arguments = [];
205
206 15
        foreach ($parameters as $parameter) {
207
            if (
208 5
                $parameter->hasType()
209 5
                && $parameter->getType() instanceof ReflectionNamedType
210 5
                && !$parameter->getType()->isBuiltin()
211
            ) {
212 5
                if ($parameter->getType()->getName() === ServerRequestInterface::class) {
213 5
                    $arguments[$parameter->getName()] = $request;
214 5
                } elseif ($parameter->getType()->getName() === RequestHandlerInterface::class) {
215 5
                    $arguments[$parameter->getName()] = $handler;
216
                }
217 2
            } elseif (array_key_exists($parameter->getName(), $request->getAttributes())) {
218 2
                $arguments[$parameter->getName()] = $request->getAttribute($parameter->getName());
219
            }
220
        }
221 15
        return $arguments;
222
    }
223
}
224