Passed
Push — master ( 6915ff...aa5379 )
by Rustam
03:30 queued 01:01
created

MiddlewareFactory.php$0 ➔ __debugInfo()   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 29
    public function __construct(
35
        private ContainerInterface $container,
36
        private ?ParametersResolverInterface $parametersResolver = null
37
    ) {
38 29
    }
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
     * - Any callable.
48
     * - A controller handler action in format `[TestController::class, 'index']`. `TestController` instance will
49
     *   be created and `index()` method will be executed.
50
     * - A function returning a middleware. The middleware returned will be executed.
51
     *
52
     * For handler action and callable
53
     * typed parameters are automatically injected using dependency injection container.
54
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
55
     * and {@see RequestHandlerInterface}.
56
     *
57
     * @throws InvalidMiddlewareDefinitionException
58
     */
59 26
    public function create(array|callable|string $middlewareDefinition): MiddlewareInterface
60
    {
61 26
        if ($this->isMiddlewareClassDefinition($middlewareDefinition)) {
62
            /** @var MiddlewareInterface */
63 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

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