Passed
Push — master ( aead5c...b6ecaa )
by Sergei
12:09
created

MiddlewareFactory.php$0 ➔ getCallableParameters()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 5
ccs 2
cts 2
cp 1
rs 10
cc 1
crap 1
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 callable with
46
     *   `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface`
47
     *   signature.
48
     * - Any callable.
49
     * - A controller handler action in format `[TestController::class, 'index']`. `TestController` instance will
50
     *   be created and `index()` method will be executed.
51
     * - A function returning a middleware. The middleware returned will be executed.
52
     *
53
     * For handler action and callable
54
     * typed parameters are automatically injected using dependency injection container.
55
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
56
     * and {@see RequestHandlerInterface}.
57
     *
58
     * @throws InvalidMiddlewareDefinitionException
59 26
     */
60
    public function create(array|callable|string $middlewareDefinition): MiddlewareInterface
61 26
    {
62
        if ($this->isMiddlewareClassDefinition($middlewareDefinition)) {
63 3
            /** @var MiddlewareInterface */
64
            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

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