Passed
Push — master ( 996d29...b5f106 )
by Rustam
07:54 queued 05:44
created

MiddlewareFactory.php$1 ➔ resolveHandlerArguments()   B

Complexity

Conditions 9

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 9

Importance

Changes 5
Bugs 4 Features 0
Metric Value
c 5
b 4
f 0
dl 0
loc 30
ccs 17
cts 17
cp 1
rs 8.0555
cc 9
crap 9
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 26
    public function __construct(ContainerInterface $container)
30
    {
31 26
        $this->container = $container;
32 26
    }
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 23
    public function create($middlewareDefinition): MiddlewareInterface
50
    {
51 23
        $this->validateMiddleware($middlewareDefinition);
52
53 15
        if (is_string($middlewareDefinition)) {
54
            /** @var MiddlewareInterface */
55 3
            return $this->container->get($middlewareDefinition);
56
        }
57
58 13
        return $this->wrapCallable($middlewareDefinition);
59
    }
60
61
    /**
62
     * @param array|callable $callback
63
     */
64 13
    private function wrapCallable($callback): MiddlewareInterface
65
    {
66 13
        if (is_array($callback)) {
67 5
            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 5
                    [$this->class, $this->method] = $callback;
76 5
                    $this->container = $container;
77 5
                    $this->callback = $callback;
78 5
                }
79
80
                public function process(
81
                    ServerRequestInterface $request,
82
                    RequestHandlerInterface $handler
83
                ): ResponseInterface {
84
                    /** @var mixed $controller */
85 5
                    $controller = $this->container->get($this->class);
86
87
                    /** @var mixed $response */
88 5
                    $response = (new Injector($this->container))
89 5
                        ->invoke([$controller, $this->method], [$request, $handler]);
90 5
                    if ($response instanceof ResponseInterface) {
91 4
                        return $response;
92
                    }
93
94 1
                    throw new InvalidMiddlewareDefinitionException($this->callback);
95
                }
96
            };
97
        }
98
99
        /** @var callable $callback */
100
101 8
        return new class ($callback, $this->container) implements MiddlewareInterface {
102
            private ContainerInterface $container;
103
            private $callback;
104
105
            public function __construct(callable $callback, ContainerInterface $container)
106
            {
107 8
                $this->callback = $callback;
108 8
                $this->container = $container;
109 8
            }
110
111
            public function process(
112
                ServerRequestInterface $request,
113
                RequestHandlerInterface $handler
114
            ): ResponseInterface {
115
                /** @var mixed $response */
116 8
                $response = (new Injector($this->container))->invoke($this->callback, [$request, $handler]);
117 8
                if ($response instanceof ResponseInterface) {
118 5
                    return $response;
119
                }
120 3
                if ($response instanceof MiddlewareInterface) {
121 2
                    return $response->process($request, $handler);
122
                }
123 1
                throw new InvalidMiddlewareDefinitionException($this->callback);
124
            }
125
        };
126
    }
127
128
    /**
129
     * @param array|callable|string $middlewareDefinition A name of PSR-15 middleware, a callable with
130
     * `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface` signature or
131
     * a handler action (an array of [handlerClass, handlerMethod]). For handler action and callable typed parameters
132
     * are automatically injected using dependency injection container passed to the route.
133
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
134
     * and {@see RequestHandlerInterface}.
135
     *
136
     * @throws InvalidMiddlewareDefinitionException
137
     */
138 23
    private function validateMiddleware($middlewareDefinition): void
139
    {
140 23
        if (is_string($middlewareDefinition) && is_subclass_of($middlewareDefinition, MiddlewareInterface::class)) {
141 3
            return;
142
        }
143
144 21
        if ($this->isCallable($middlewareDefinition)) {
145 13
            return;
146
        }
147
148 8
        throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
149
    }
150
151
    /**
152
     * @param mixed $definition
153
     */
154 21
    private function isCallable($definition): bool
155
    {
156 21
        if ($definition instanceof Closure) {
157 8
            return true;
158
        }
159
160 13
        return is_array($definition)
161 13
            && array_keys($definition) === [0, 1]
162 13
            && is_string($definition[0])
163 13
            && is_string($definition[1])
164 6
            && in_array(
165 6
                $definition[1],
166 6
                class_exists($definition[0]) ? get_class_methods($definition[0]) : [],
167 13
                true
168
            );
169
    }
170
}
171