Passed
Pull Request — master (#36)
by Rustam
02:17
created

anonymous//src/MiddlewareFactory.php$0   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 50
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 10
c 1
b 0
f 0
dl 0
loc 50
ccs 22
cts 22
cp 1
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\Http\Message\ResponseInterface;
10
use Psr\Http\Message\ServerRequestInterface;
11
use Psr\Http\Server\MiddlewareInterface;
12
use Psr\Http\Server\RequestHandlerInterface;
13
use Yiisoft\Injector\Injector;
14
15
use function in_array;
16
use function is_array;
17
use function is_string;
18
19
/**
20
 * Creates a PSR-15 middleware based on the definition provided.
21
 */
22
final class MiddlewareFactory implements MiddlewareFactoryInterface
23
{
24
    private ContainerInterface $container;
25
26
    /**
27
     * @param ContainerInterface $container Container to use for resolving definitions.
28
     */
29 28
    public function __construct(ContainerInterface $container)
30
    {
31 28
        $this->container = $container;
32 28
    }
33
34
    /**
35
     * @param array|callable|string $middlewareDefinition Middleware definition in one of the following formats:
36
     *
37
     * - A name of PSR-15 middleware class. The middleware instance will be obtained from container and executed.
38
     * - A callable with `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface`
39
     *   signature.
40
     * - A controller handler action in format `[TestController::class, 'index']`. `TestController` instance will
41
     *   be created and `index()` method will be executed.
42
     * - A function returning a middleware. The middleware returned will be executed.
43
     *
44
     * For handler action and callable
45
     * typed parameters are automatically injected using dependency injection container.
46
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
47
     * and {@see RequestHandlerInterface}.
48
     */
49 25
    public function create($middlewareDefinition): MiddlewareInterface
50
    {
51 25
        $this->validateMiddleware($middlewareDefinition);
52
53 17
        if (is_string($middlewareDefinition)) {
54
            /** @var MiddlewareInterface */
55 3
            return $this->container->get($middlewareDefinition);
56
        }
57
58 15
        return $this->wrapCallable($middlewareDefinition);
59
    }
60
61
    /**
62
     * @param array|callable $callback
63
     */
64 15
    private function wrapCallable($callback): MiddlewareInterface
65
    {
66 15
        if (is_array($callback)) {
67 6
            return new class($this->container, $callback) implements MiddlewareInterface {
68
                private string $class;
69
                private string $method;
70
                private ContainerInterface $container;
71
                private array $callback;
72
73
                public function __construct(ContainerInterface $container, array $callback)
74
                {
75 6
                    [$this->class, $this->method] = $callback;
76 6
                    $this->container = $container;
77 6
                    $this->callback = $callback;
78 6
                }
79
80
                public function process(
81
                    ServerRequestInterface $request,
82
                    RequestHandlerInterface $handler
83
                ): ResponseInterface {
84
                    /** @var mixed $controller */
85 6
                    $controller = $this->container->get($this->class);
86
87
                    /** @var mixed $response */
88 6
                    $response = (new Injector($this->container))
89 6
                        ->invoke([$controller, $this->method], $this->resolveHandlerArguments($request, $handler));
90 6
                    if ($response instanceof ResponseInterface) {
91 5
                        return $response;
92
                    }
93
94 1
                    throw new InvalidMiddlewareDefinitionException($this->callback);
95
                }
96
97
                private function resolveHandlerArguments(
98
                    ServerRequestInterface $request,
99
                    RequestHandlerInterface $handler
100
                ): array {
101 6
                    $parameters = (new \ReflectionClass($this->class))->getMethod($this->method)->getParameters();
102 6
                    $arguments = [];
103
104 6
                    foreach ($parameters as $parameter) {
105 2
                        if ($parameter->getType()->getName() === ServerRequestInterface::class) {
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
                        if ($parameter->getType()->/** @scrutinizer ignore-call */ getName() === ServerRequestInterface::class) {
Loading history...
106 2
                            $arguments[$parameter->getName()] = $request;
107 2
                        } else if ($parameter->getType()->getName() === RequestHandlerInterface::class) {
108 2
                            $arguments[$parameter->getName()] = $handler;
109
                        } else if (
110 1
                            (!$parameter->hasType() || $parameter->getType()->isBuiltin())
111 1
                            && array_key_exists($parameter->getName(), $request->getAttributes())
112
                        ) {
113 1
                            $arguments[$parameter->getName()] = $request->getAttribute($parameter->getName());
114
                        }
115
                    }
116 6
                    return $arguments;
117
                }
118
            };
119
        }
120
121
        /** @var callable $callback */
122
123 9
        return new class($callback, $this->container) implements MiddlewareInterface {
124
            private ContainerInterface $container;
125
            private $callback;
126
127
            public function __construct(callable $callback, ContainerInterface $container)
128
            {
129 9
                $this->callback = $callback;
130 9
                $this->container = $container;
131 9
            }
132
133
            public function process(
134
                ServerRequestInterface $request,
135
                RequestHandlerInterface $handler
136
            ): ResponseInterface {
137
                /** @var mixed $response */
138 9
                $response = (new Injector($this->container))->invoke(
139 9
                    $this->callback,
140 9
                    $this->resolveHandlerArguments($request, $handler)
141
                );
142 9
                if ($response instanceof ResponseInterface) {
143 6
                    return $response;
144
                }
145 3
                if ($response instanceof MiddlewareInterface) {
146 2
                    return $response->process($request, $handler);
147
                }
148 1
                throw new InvalidMiddlewareDefinitionException($this->callback);
149
            }
150
151
            private function resolveHandlerArguments(
152
                ServerRequestInterface $request,
153
                RequestHandlerInterface $handler
154
            ): array {
155 9
                $parameters = (new \ReflectionFunction($this->callback))->getParameters();
156 9
                $arguments = [];
157
158 9
                foreach ($parameters as $parameter) {
159 3
                    if ($parameter->hasType() && $parameter->getType()->getName() === ServerRequestInterface::class) {
160 3
                        $arguments[$parameter->getName()] = $request;
161 3
                    } else if ($parameter->hasType() && $parameter->getType()->getName() === RequestHandlerInterface::class) {
162 3
                        $arguments[$parameter->getName()] = $handler;
163
                    } else if (
164 1
                        (!$parameter->hasType() || $parameter->getType()->isBuiltin())
165 1
                        && array_key_exists($parameter->getName(), $request->getAttributes())
166
                    ) {
167 1
                        $arguments[$parameter->getName()] = $request->getAttribute($parameter->getName());
168
                    }
169
                }
170 9
                return $arguments;
171
            }
172
        };
173
    }
174
175
    /**
176
     * @param array|callable|string $middlewareDefinition A name of PSR-15 middleware, a callable with
177
     * `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface` signature or
178
     * a handler action (an array of [handlerClass, handlerMethod]). For handler action and callable typed parameters
179
     * are automatically injected using dependency injection container passed to the route.
180
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
181
     * and {@see RequestHandlerInterface}.
182
     *
183
     * @throws InvalidMiddlewareDefinitionException
184
     */
185 25
    private function validateMiddleware($middlewareDefinition): void
186
    {
187 25
        if (is_string($middlewareDefinition) && is_subclass_of($middlewareDefinition, MiddlewareInterface::class)) {
188 3
            return;
189
        }
190
191 23
        if ($this->isCallable($middlewareDefinition)) {
192 15
            return;
193
        }
194
195 8
        throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
196
    }
197
198
    /**
199
     * @param mixed $definition
200
     */
201 23
    private function isCallable($definition): bool
202
    {
203 23
        if ($definition instanceof Closure) {
204 9
            return true;
205
        }
206
207 14
        return is_array($definition)
208 14
            && array_keys($definition) === [0, 1]
209 14
            && is_string($definition[0])
210 14
            && is_string($definition[1])
211 7
            && in_array(
212 7
                $definition[1],
213 7
                class_exists($definition[0]) ? get_class_methods($definition[0]) : [],
214 14
                true
215
            );
216
    }
217
}
218