Passed
Pull Request — master (#79)
by Sergei
12:24
created

MiddlewareFactory::createCallableWrapper()

Size

Total Lines 53
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 21
c 2
b 0
f 0
dl 0
loc 53
ccs 18
cts 18
cp 1
nc 1
nop 1

3 Methods

Rating   Name   Duplication   Size   Complexity  
A MiddlewareFactory.php$0 ➔ __debugInfo() 0 3 1
A MiddlewareFactory.php$0 ➔ process() 0 22 4
A MiddlewareFactory.php$0 ➔ __construct() 0 12 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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