Test Failed
Pull Request — master (#93)
by Alexander
05:13 queued 02:34
created

MiddlewareFactory::withEventDispatcher()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
ccs 0
cts 0
cp 0
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\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
    /**
52
     * @param array|callable|string $middlewareDefinition Middleware definition in one of the following formats:
53
     *
54
     * - A name of PSR-15 middleware class. The middleware instance will be obtained from container and executed.
55
     * - A name of PSR-15 request handler class. The request handler instance will be obtained from container and executed.
56
     * - A name of invokable class. The invokable class instance will be obtained from container and executed.
57
     * - A callable with
58
     *   `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface`
59 26
     *   signature.
60
     * - Any callable.
61 26
     * - A controller handler action in format `[TestController::class, 'index']`. `TestController` instance will
62
     *   be created and `index()` method will be executed.
63 3
     * - A function returning a middleware. The middleware returned will be executed.
64
     *
65
     * For handler action and callable
66 24
     * typed parameters are automatically injected using dependency injection container.
67 17
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
68
     * and {@see RequestHandlerInterface}.
69
     *
70 7
     * @throws InvalidMiddlewareDefinitionException
71
     */
72
    public function create(array|callable|string $middlewareDefinition): MiddlewareInterface
73
    {
74
        if ($this->isMiddlewareClassDefinition($middlewareDefinition)) {
75 1
            /** @var MiddlewareInterface */
76
            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

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