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