Test Failed
Pull Request — master (#93)
by Dmitriy
11:27
created

MiddlewareFactory::isArrayDefinition()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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

77
            return $this->container->get(/** @scrutinizer ignore-type */ $middlewareDefinition);
Loading history...
78 6
        }
79
80
        if ($this->isRequestHandlerClassDefinition($middlewareDefinition)) {
81
            return $this->wrapCallable([$middlewareDefinition, 'handle']);
82
        }
83
84 26
        if ($this->isInvokableClassDefinition($middlewareDefinition)) {
85
            return $this->wrapCallable([$middlewareDefinition, '__invoke']);
86 26
        }
87 26
88
        if ($this->isCallableDefinition($middlewareDefinition)) {
89
            return $this->wrapCallable($middlewareDefinition);
90
        }
91
92
        if ($this->isArrayDefinition($middlewareDefinition)) {
93 24
            /**
94
             * @var MiddlewareInterface
95 24
             *
96 11
             * @psalm-suppress InvalidArgument Need for Psalm version 4.* only.
97
             */
98
            return ArrayDefinition::fromConfig($middlewareDefinition)->resolve($this->container);
99 13
        }
100 13
101 13
        throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
102 13
    }
103 13
104 13
    /**
105 13
     * @psalm-assert-if-true class-string<MiddlewareInterface> $definition
106 13
     */
107 13
    private function isMiddlewareClassDefinition(array|callable|string $definition): bool
108
    {
109
        return is_string($definition)
110
            && is_subclass_of($definition, MiddlewareInterface::class);
111
    }
112
113 7
    /**
114
     * @psalm-assert-if-true class-string<RequestHandlerInterface> $definition
115 7
     */
116 2
    private function isRequestHandlerClassDefinition(array|callable|string $definition): bool
117
    {
118
        return is_string($definition)
119
            && is_subclass_of($definition, RequestHandlerInterface::class);
120 5
    }
121 4
122 4
    /**
123
     * @psalm-assert-if-true class-string|object $definition
124
     */
125 1
    private function isInvokableClassDefinition(array|callable|string $definition): bool
126
    {
127
        /**
128
         * @psalm-suppress ArgumentTypeCoercion `method_exists()` allow use any string as first argument and returns
129
         * false if it not class name.
130
         */
131 17
        return is_string($definition) && method_exists($definition, '__invoke');
132
    }
133 17
134 11
    /**
135
     * @psalm-assert-if-true array{0:class-string, 1:non-empty-string}|callable $definition
136
     */
137 6
    private function isCallableDefinition(array|callable|string $definition): bool
138
    {
139
        if (is_callable($definition)) {
140 11
            return true;
141
        }
142 11
143
        return is_array($definition)
144
            && array_keys($definition) === [0, 1]
145
            && is_string($definition[0])
146
            && is_string($definition[1])
147
            && in_array(
148
                $definition[1],
149
                class_exists($definition[0]) ? get_class_methods($definition[0]) : [],
150 11
                true
151
            );
152
    }
153
154
    /**
155
     * @psalm-assert-if-true ArrayDefinitionConfig $definition
156
     */
157 11
    private function isArrayDefinition(array|callable|string $definition): bool
158 11
    {
159 1
        if (!is_array($definition)) {
0 ignored issues
show
introduced by
The condition is_array($definition) is always true.
Loading history...
160 1
            return false;
161 1
        }
162 1
163
        try {
164
            DefinitionValidator::validateArrayDefinition($definition);
165 11
        } catch (InvalidConfigException) {
166 11
            return false;
167 8
        }
168
169 3
        return is_subclass_of((string) ($definition['class'] ?? ''), MiddlewareInterface::class);
170 2
    }
171
172 1
    /**
173
     * @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...
174
     */
175
    private function wrapCallable(array|callable $callable): MiddlewareInterface
176
    {
177 1
        if (is_callable($callable)) {
178
            return $this->createCallableWrapper($callable);
179
        }
180
181
        return $this->createCallableWrapper([$this->container->get($callable[0]), $callable[1]]);
182
    }
183
184
    private function createCallableWrapper(callable $callback): MiddlewareInterface
185 1
    {
186
        return new class (
187 1
            $callback,
188
            $this->container,
189 11
            $this,
190
            $this->eventDispatcher,
191
            $this->parametersResolver,
192
        ) implements MiddlewareInterface {
193
            /** @var callable */
194
            private $callback;
195
            /**
196 6
             * @var ReflectionParameter[]
197
             * @psalm-var array<string,ReflectionParameter>
198 6
             */
199
            private array $callableParameters = [];
200
            public array $middlewares;
201
202
            public function __construct(
203
                callable $callback,
204
                private ContainerInterface $container,
205
                private MiddlewareFactory $middlewareFactory,
206
                private ?EventDispatcherInterface $eventDispatcher,
207 6
                private ?ParametersResolverInterface $parametersResolver
208
            ) {
209
                $this->callback = $callback;
210
                $callback = Closure::fromCallable($callback);
211
212
                $reflectionFunction = new ReflectionFunction($callback);
213
214 6
                $this->middlewares = array_map(
215 6
                    static fn (ReflectionAttribute $attribute) => $attribute->newInstance()->definition,
216 6
                    $reflectionFunction->getAttributes(Middleware::class)
217 1
                );
218 1
                $callableParameters = $reflectionFunction->getParameters();
219 1
                foreach ($callableParameters as $parameter) {
220 1
                    $this->callableParameters[$parameter->getName()] = $parameter;
221
                }
222
            }
223
224 6
            public function process(
225 6
                ServerRequestInterface $request,
226 5
                RequestHandlerInterface $handler
227
            ): ResponseInterface {
228
                $parameters = [$request, $handler];
229 1
                if ($this->parametersResolver !== null) {
230
                    $parameters = array_merge(
231
                        $parameters,
232
                        $this->parametersResolver->resolve($this->callableParameters, $request)
233
                    );
234
                }
235
236
                if ($this->middlewares !== []) {
237
                    $middlewares = [...$this->middlewares, fn() => ($this->callback)()];
238
                    $middlewareDispatcher = new MiddlewareDispatcher($this->middlewareFactory, $this->eventDispatcher);
239 1
                    $middlewareDispatcher = $middlewareDispatcher->withMiddlewares($middlewares);
240
                    $response = $middlewareDispatcher->dispatch($request, $handler);
241
                } else {
242
                    /** @var MiddlewareInterface|mixed|ResponseInterface $response */
243
                    $response = (new Injector($this->container))->invoke($this->callback, $parameters);
244 1
                }
245 1
                if ($response instanceof ResponseInterface) {
246 1
                    return $response;
247
                }
248 6
                if ($response instanceof MiddlewareInterface) {
249
                    return $response->process($request, $handler);
250
                }
251
252
                throw new InvalidMiddlewareDefinitionException($this->callback);
253
            }
254
255
            public function __debugInfo(): array
256
            {
257
                if (is_array($this->callback)
258
                    && isset($this->callback[0], $this->callback[1])
259
                    && is_object($this->callback[0])
260
                    && is_string($this->callback[1])
261
                ) {
262
                    return ['callback' => [$this->callback[0]::class, $this->callback[1]]];
263
                }
264
                return ['callback' => $this->callback];
265
            }
266
        };
267
    }
268
}
269