Passed
Push — master ( 560900...ab8115 )
by Sergei
02:39
created

isRequestHandlerClassDefinition()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
eloc 2
nc 2
nop 1
cc 2
crap 2
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 ReflectionClass;
14
use ReflectionFunction;
15
use ReflectionParameter;
16
use Yiisoft\Definitions\ArrayDefinition;
17
use Yiisoft\Definitions\Exception\InvalidConfigException;
18
use Yiisoft\Definitions\Helpers\DefinitionValidator;
19
use Yiisoft\Injector\Injector;
20
21
use function in_array;
22
use function is_array;
23
use function is_string;
24
25
/**
26
 * Creates a PSR-15 middleware based on the definition provided.
27
 *
28
 * @psalm-import-type ArrayDefinitionConfig from ArrayDefinition
29
 */
30
final class MiddlewareFactory
31
{
32
    /**
33
     * @param ContainerInterface $container Container to use for resolving definitions.
34 29
     */
35
    public function __construct(
36
        private ContainerInterface $container,
37
        private ?ParametersResolverInterface $parametersResolver = null
38 29
    ) {
39
    }
40
41
    /**
42
     * @param array|callable|string $middlewareDefinition Middleware definition in one of the following formats:
43
     *
44
     * - A name of PSR-15 middleware class. The middleware instance will be obtained from container and executed.
45
     * - A name of PSR-15 request handler class. The request handler instance will be obtained from container and executed.
46
     * - A name of invokable class. The invokable class instance will be obtained from container and executed.
47
     * - A callable with
48
     *   `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface`
49
     *   signature.
50
     * - Any callable.
51
     * - A controller handler action in format `[TestController::class, 'index']`. `TestController` instance will
52
     *   be created and `index()` method will be executed.
53
     * - A function returning a middleware. The middleware returned will be executed.
54
     *
55
     * For handler action and callable
56
     * typed parameters are automatically injected using dependency injection container.
57
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
58
     * and {@see RequestHandlerInterface}.
59 26
     *
60
     * @throws InvalidMiddlewareDefinitionException
61 26
     */
62
    public function create(array|callable|string $middlewareDefinition): MiddlewareInterface
63 3
    {
64
        if ($this->isMiddlewareClassDefinition($middlewareDefinition)) {
65
            /** @var MiddlewareInterface */
66 24
            return $this->container->get($middlewareDefinition);
0 ignored issues
show
Bug introduced by
$middlewareDefinition of type array is incompatible with the type string expected by parameter $id of Psr\Container\ContainerInterface::get(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

66
            return $this->container->get(/** @scrutinizer ignore-type */ $middlewareDefinition);
Loading history...
67 17
        }
68
69
        if ($this->isRequestHandlerClassDefinition($middlewareDefinition)) {
70 7
            return $this->wrapCallable([$middlewareDefinition, 'handle']);
71
        }
72
73
        if ($this->isInvokableClassDefinition($middlewareDefinition)) {
74
            return $this->wrapCallable([$middlewareDefinition, '__invoke']);
75 1
        }
76
77
        if ($this->isCallableDefinition($middlewareDefinition)) {
78 6
            return $this->wrapCallable($middlewareDefinition);
79
        }
80
81
        if ($this->isArrayDefinition($middlewareDefinition)) {
82
            /**
83
             * @var MiddlewareInterface
84 26
             *
85
             * @psalm-suppress InvalidArgument Need for Psalm version 4.* only.
86 26
             */
87 26
            return ArrayDefinition::fromConfig($middlewareDefinition)->resolve($this->container);
88
        }
89
90
        throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
91
    }
92
93 24
    /**
94
     * @psalm-assert-if-true class-string<MiddlewareInterface> $definition
95 24
     */
96 11
    private function isMiddlewareClassDefinition(array|callable|string $definition): bool
97
    {
98
        return is_string($definition)
99 13
            && is_subclass_of($definition, MiddlewareInterface::class);
100 13
    }
101 13
102 13
    /**
103 13
     * @psalm-assert-if-true class-string<RequestHandlerInterface> $definition
104 13
     */
105 13
    private function isRequestHandlerClassDefinition(array|callable|string $definition): bool
106 13
    {
107 13
        return is_string($definition)
108
            && is_subclass_of($definition, RequestHandlerInterface::class);
109
    }
110
111
    /**
112
     * @psalm-assert-if-true class-string|object $definition
113 7
     */
114
    private function isInvokableClassDefinition(array|callable|string $definition): bool
115 7
    {
116 2
        /**
117
         * @psalm-suppress ArgumentTypeCoercion `method_exists()` allow use any string as first argument and returns
118
         * false if it not class name.
119
         */
120 5
        return is_string($definition) && method_exists($definition, '__invoke');
121 4
    }
122 4
123
    /**
124
     * @psalm-assert-if-true array{0:class-string, 1:non-empty-string}|callable $definition
125 1
     */
126
    private function isCallableDefinition(array|callable|string $definition): bool
127
    {
128
        if (is_callable($definition)) {
129
            return true;
130
        }
131 17
132
        return is_array($definition)
133 17
            && array_keys($definition) === [0, 1]
134 11
            && is_string($definition[0])
135
            && is_string($definition[1])
136
            && in_array(
137 6
                $definition[1],
138
                class_exists($definition[0]) ? get_class_methods($definition[0]) : [],
139
                true
140 11
            );
141
    }
142 11
143
    /**
144
     * @psalm-assert-if-true ArrayDefinitionConfig $definition
145
     */
146
    private function isArrayDefinition(array|callable|string $definition): bool
147
    {
148
        if (!is_array($definition)) {
0 ignored issues
show
introduced by
The condition is_array($definition) is always true.
Loading history...
149
            return false;
150 11
        }
151
152
        try {
153
            DefinitionValidator::validateArrayDefinition($definition);
154
        } catch (InvalidConfigException) {
155
            return false;
156
        }
157 11
158 11
        return is_subclass_of((string) ($definition['class'] ?? ''), MiddlewareInterface::class);
159 1
    }
160 1
161 1
    /**
162 1
     * @param array{0:class-string, 1:non-empty-string}|callable $callable
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{0:class-string, 1:non-empty-string}|callable at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array{0:class-string, 1:non-empty-string}|callable.
Loading history...
163
     */
164
    private function wrapCallable(array|callable $callable): MiddlewareInterface
165 11
    {
166 11
        if (is_callable($callable)) {
167 8
            return $this->createCallableWrapper($callable);
168
        }
169 3
170 2
        return $this->createActionWrapper($callable[0], $callable[1]);
171
    }
172 1
173
    private function createCallableWrapper(callable $callback): MiddlewareInterface
174
    {
175
        return new class ($callback, $this->container, $this->parametersResolver) implements MiddlewareInterface {
176
            /** @var callable */
177 1
            private $callback;
178
            /**
179
             * @var ReflectionParameter[]
180
             * @psalm-var array<string,ReflectionParameter>
181
             */
182
            private array $callableParameters = [];
183
184
            public function __construct(
185 1
                callable $callback,
186
                private ContainerInterface $container,
187 1
                private ?ParametersResolverInterface $parametersResolver
188
            ) {
189 11
                $this->callback = $callback;
190
                $callback = Closure::fromCallable($callback);
191
192
                $callableParameters = (new ReflectionFunction($callback))->getParameters();
193
                foreach ($callableParameters as $parameter) {
194
                    $this->callableParameters[$parameter->getName()] = $parameter;
195
                }
196 6
            }
197
198 6
            public function process(
199
                ServerRequestInterface $request,
200
                RequestHandlerInterface $handler
201
            ): ResponseInterface {
202
                $parameters = [$request, $handler];
203
                if ($this->parametersResolver !== null) {
204
                    $parameters = array_merge(
205
                        $parameters,
206
                        $this->parametersResolver->resolve($this->callableParameters, $request)
207 6
                    );
208
                }
209
210
                /** @var MiddlewareInterface|mixed|ResponseInterface $response */
211
                $response = (new Injector($this->container))->invoke($this->callback, $parameters);
212
                if ($response instanceof ResponseInterface) {
213
                    return $response;
214 6
                }
215 6
                if ($response instanceof MiddlewareInterface) {
216 6
                    return $response->process($request, $handler);
217 1
                }
218 1
219 1
                throw new InvalidMiddlewareDefinitionException($this->callback);
220 1
            }
221
222
            public function __debugInfo(): array
223
            {
224 6
                return ['callback' => $this->callback];
225 6
            }
226 5
        };
227
    }
228
229 1
    /**
230
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
231
     * @param non-empty-string $method
232
     */
233
    private function createActionWrapper(string $class, string $method): MiddlewareInterface
234
    {
235
        return new class ($this->container, $this->parametersResolver, $class, $method) implements MiddlewareInterface {
236
            /**
237
             * @var ReflectionParameter[]
238
             * @psalm-var array<string,ReflectionParameter>
239 1
             */
240
            private array $actionParameters = [];
241
242
            public function __construct(
243
                private ContainerInterface $container,
244 1
                private ?ParametersResolverInterface $parametersResolver,
245 1
                /** @var class-string */
246 1
                private string $class,
247
                /** @var non-empty-string */
248 6
                private string $method
249
            ) {
250
                $actionParameters = (new ReflectionClass($this->class))->getMethod($this->method)->getParameters();
251
                foreach ($actionParameters as $parameter) {
252
                    $this->actionParameters[$parameter->getName()] = $parameter;
253
                }
254
            }
255
256
            public function process(
257
                ServerRequestInterface $request,
258
                RequestHandlerInterface $handler
259
            ): ResponseInterface {
260
                /** @var mixed $controller */
261
                $controller = $this->container->get($this->class);
262
                $parameters = [$request, $handler];
263
                if ($this->parametersResolver !== null) {
264
                    $parameters = array_merge(
265
                        $parameters,
266
                        $this->parametersResolver->resolve($this->actionParameters, $request)
267
                    );
268
                }
269
270
                /** @var mixed|ResponseInterface $response */
271
                $response = (new Injector($this->container))->invoke([$controller, $this->method], $parameters);
272
                if ($response instanceof ResponseInterface) {
273
                    return $response;
274
                }
275
276
                throw new InvalidMiddlewareDefinitionException([$this->class, $this->method]);
277
            }
278
279
            public function __debugInfo()
280
            {
281
                return [
282
                    'callback' => [$this->class, $this->method],
283
                ];
284
            }
285
        };
286
    }
287
}
288