Passed
Pull Request — master (#75)
by Sergei
02:35
created

MiddlewareFactory.php$1 ➔ createActionWrapper()   A

Complexity

Conditions 1

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 46
ccs 14
cts 14
cp 1
rs 9.1781
cc 1
crap 1

3 Methods

Rating   Name   Duplication   Size   Complexity  
A MiddlewareFactory.php$1 ➔ __construct() 0 11 1
A MiddlewareFactory.php$1 ➔ process() 0 21 3
A MiddlewareFactory.php$1 ➔ __debugInfo() 0 4 1
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 ReflectionParameter;
16
use Yiisoft\Definitions\ArrayDefinition;
17
use Yiisoft\Definitions\Exception\InvalidConfigException;
18
use Yiisoft\Definitions\Helpers\DefinitionValidator;
19
use Yiisoft\Injector\Injector;
20
21
use function in_array;
22
use function is_array;
23
use function is_string;
24
25
/**
26
 * Creates a PSR-15 middleware based on the definition provided.
27
 *
28
 * @psalm-import-type ArrayDefinitionConfig from ArrayDefinition
29
 */
30
final class MiddlewareFactory
31
{
32
    /**
33
     * @param ContainerInterface $container Container to use for resolving definitions.
34 29
     */
35
    public function __construct(
36
        private ContainerInterface $container,
37
        private ?ParametersResolverInterface $parametersResolver = null
38 29
    ) {
39
    }
40
41
    /**
42
     * @param array|callable|string $middlewareDefinition Middleware definition in one of the following formats:
43
     *
44
     * - A name of PSR-15 middleware class. The middleware instance will be obtained from container and executed.
45
     * - A callable with
46
     *   `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface`
47
     *   signature.
48
     * - Any callable.
49
     * - A controller handler action in format `[TestController::class, 'index']`. `TestController` instance will
50
     *   be created and `index()` method will be executed.
51
     * - A function returning a middleware. The middleware returned will be executed.
52
     *
53
     * For handler action and callable
54
     * typed parameters are automatically injected using dependency injection container.
55
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
56
     * and {@see RequestHandlerInterface}.
57
     *
58
     * @throws InvalidMiddlewareDefinitionException
59 26
     */
60
    public function create(array|callable|string $middlewareDefinition): MiddlewareInterface
61 26
    {
62
        if ($this->isMiddlewareClassDefinition($middlewareDefinition)) {
63 3
            /** @var MiddlewareInterface */
64
            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

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