Passed
Pull Request — master (#68)
by Sergei
02:26
created

MiddlewareFactory.php$1 ➔ __construct()   A

Complexity

Conditions 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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