Passed
Pull Request — master (#68)
by Rustam
02:37
created

MiddlewareFactory.php$1 ➔ __debugInfo()   A

Complexity

Conditions 1

Size

Total Lines 4

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 4
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 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
     * @return MiddlewareInterface
133
     */
134 13
    private function wrap($callable): MiddlewareInterface
135
    {
136 13
        if (is_array($callable)) {
137 5
            return $this->createActionWrapper($callable[0], $callable[1]);
138
        }
139
140 8
        return $this->createCallableWrapper($callable);
141
    }
142
143 8
    private function createCallableWrapper(callable $callback): MiddlewareInterface
144
    {
145 8
        return new class ($callback, $this->container, $this->parametersResolver) implements MiddlewareInterface {
146
            private $callback;
147
148
            public function __construct(
149
                callable $callback,
150
                private ContainerInterface $container,
151
                private ParametersResolverInterface $parametersResolver
152
            ) {
153 8
                $this->callback = $callback;
154
            }
155
156
            public function process(
157
                ServerRequestInterface $request,
158
                RequestHandlerInterface $handler
159
            ): ResponseInterface {
160 8
                $parameters = array_merge(
161 8
                    [$request, $handler],
162 8
                    $this->parametersResolver->resolve($this->getCallableParameters(), $request)
163 8
                );
164
                /** @var MiddlewareInterface|mixed|ResponseInterface $response */
165 8
                $response = (new Injector($this->container))->invoke($this->callback, $parameters);
166 8
                if ($response instanceof ResponseInterface) {
167 5
                    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
            /**
176
             * @return \ReflectionParameter[]
177
             */
178
            private function getCallableParameters(): array
179
            {
180 8
                $callback = Closure::fromCallable($this->callback);
181
182 8
                return (new ReflectionFunction($callback))->getParameters();
183
            }
184 8
        };
185
    }
186
187
    /**
188
     * @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...
189
     * @param string $method
190
     *
191
     * @return MiddlewareInterface
192
     */
193 5
    private function createActionWrapper(string $class, string $method): MiddlewareInterface
194
    {
195 5
        return new class ($this->container, $this->parametersResolver, $class, $method) implements MiddlewareInterface {
196
            public function __construct(
197
                private ContainerInterface $container,
198
                private ParametersResolverInterface $parametersResolver,
199
                /** @var class-string */
200
                private string $class,
201
                private string $method
202
            ) {
203 5
            }
204
205
            public function process(
206
                ServerRequestInterface $request,
207
                RequestHandlerInterface $handler
208
            ): ResponseInterface {
209
                /** @var mixed $controller */
210 5
                $controller = $this->container->get($this->class);
211 5
                $parameters = array_merge(
212 5
                    [$request, $handler],
213 5
                    $this->parametersResolver->resolve($this->getActionParameters(), $request)
214 5
                );
215
216
                /** @var mixed|ResponseInterface $response */
217 5
                $response = (new Injector($this->container))->invoke([$controller, $this->method], $parameters);
218 5
                if ($response instanceof ResponseInterface) {
219 4
                    return $response;
220
                }
221
222 1
                throw new InvalidMiddlewareDefinitionException([$this->class, $this->method]);
223
            }
224
225
            /**
226
             * @throws \ReflectionException
227
             *
228
             * @return \ReflectionParameter[]
229
             */
230
            private function getActionParameters(): array
231
            {
232 5
                return (new ReflectionClass($this->class))->getMethod($this->method)->getParameters();
233
            }
234
235
            public function __debugInfo()
236
            {
237 1
                return [
238 1
                    'callback' => [$this->class, $this->method],
239 1
                ];
240
            }
241 5
        };
242
    }
243
}
244