Passed
Push — master ( 0cccd5...5375ef )
by Rustam
13:11
created

MiddlewareFactory.php$1 ➔ getActionParameters()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

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