Test Failed
Pull Request — master (#93)
by Dmitriy
02:34
created

MiddlewareFactory::hasEventDispatcher()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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