Test Failed
Pull Request — master (#93)
by Dmitriy
02:44
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 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
211
                $reflectionFunction = new ReflectionFunction(Closure::fromCallable($callback));
212
213
                foreach ($reflectionFunction->getAttributes(Middleware::class) as $attribute) {
214 6
                    $this->middlewares[] = $attribute->newInstance()->getDefinition();
215 6
                }
216 6
                foreach ($reflectionFunction->getParameters() as $parameter) {
217 1
                    $this->callableParameters[$parameter->getName()] = $parameter;
218 1
                }
219 1
            }
220 1
221
            public function process(
222
                ServerRequestInterface $request,
223
                RequestHandlerInterface $handler
224 6
            ): ResponseInterface {
225 6
                $parameters = [$request, $handler];
226 5
                if ($this->parametersResolver !== null) {
227
                    $parameters = array_merge(
228
                        $parameters,
229 1
                        $this->parametersResolver->resolve($this->callableParameters, $request)
230
                    );
231
                }
232
233
                if ($this->middlewares !== []) {
234
                    $middlewares = [...$this->middlewares, fn() => ($this->callback)()];
235
                    $middlewareDispatcher = new MiddlewareDispatcher($this->middlewareFactory, $this->eventDispatcher);
236
                    $middlewareDispatcher = $middlewareDispatcher->withMiddlewares($middlewares);
237
                    $response = $middlewareDispatcher->dispatch($request, $handler);
238
                } else {
239 1
                    /** @var MiddlewareInterface|mixed|ResponseInterface $response */
240
                    $response = (new Injector($this->container))->invoke($this->callback, $parameters);
241
                }
242
                if ($response instanceof ResponseInterface) {
243
                    return $response;
244 1
                }
245 1
                if ($response instanceof MiddlewareInterface) {
246 1
                    return $response->process($request, $handler);
247
                }
248 6
249
                throw new InvalidMiddlewareDefinitionException($this->callback);
250
            }
251
252
            public function __debugInfo(): array
253
            {
254
                if (is_array($this->callback)
255
                    && isset($this->callback[0], $this->callback[1])
256
                    && is_object($this->callback[0])
257
                    && is_string($this->callback[1])
258
                ) {
259
                    return ['callback' => [$this->callback[0]::class, $this->callback[1]]];
260
                }
261
                return ['callback' => $this->callback];
262
            }
263
        };
264
    }
265
}
266