Test Failed
Pull Request — master (#53)
by Alexander
05:41 queued 03:18
created

php$1 ➔ isMiddlewareClassDefinition()   A

Complexity

Conditions 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 1
cts 1
cp 1
rs 10
cc 2
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\Http\Message\ResponseInterface;
10
use Psr\Http\Message\ServerRequestInterface;
11
use Psr\Http\Server\MiddlewareInterface;
12
use Psr\Http\Server\RequestHandlerInterface;
13
use Yiisoft\Definitions\ArrayDefinition;
14
use Yiisoft\Definitions\Exception\InvalidConfigException;
15
use Yiisoft\Definitions\Helpers\DefinitionValidator;
16
use Yiisoft\Injector\Injector;
17
18
use function in_array;
19
use function is_array;
20
use function is_string;
21
22
/**
23
 * Creates a PSR-15 middleware based on the definition provided.
24
 *
25
 * @psalm-import-type ArrayDefinitionConfig from ArrayDefinition
26
 */
27
final class MiddlewareFactory implements MiddlewareFactoryInterface
28
{
29 26
    private ContainerInterface $container;
30
31 26
    /**
32
     * @param ContainerInterface $container Container to use for resolving definitions.
33
     */
34
    public function __construct(ContainerInterface $container)
35
    {
36
        $this->container = $container;
37
    }
38
39
    /**
40
     * @param array|callable|string $middlewareDefinition Middleware definition in one of the following formats:
41
     *
42
     * - A name of PSR-15 middleware class. The middleware instance will be obtained from container and executed.
43
     * - A callable with
44
     *   `function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface`
45
     *   signature.
46
     * - A controller handler action in format `[TestController::class, 'index']`. `TestController` instance will
47
     *   be created and `index()` method will be executed.
48
     * - A function returning a middleware. The middleware returned will be executed.
49 23
     *
50
     * For handler action and callable
51 23
     * typed parameters are automatically injected using dependency injection container.
52
     * Current request and handler could be obtained by type-hinting for {@see ServerRequestInterface}
53 15
     * and {@see RequestHandlerInterface}.
54
     *
55 3
     * @throws InvalidMiddlewareDefinitionException
56
     */
57
    public function create($middlewareDefinition): MiddlewareInterface
58 13
    {
59
        if ($this->isMiddlewareClassDefinition($middlewareDefinition)) {
60
            /** @var MiddlewareInterface */
61
            return $this->container->get($middlewareDefinition);
0 ignored issues
show
Bug introduced by
It seems like $middlewareDefinition can also be of type array and callable; however, parameter $id of Psr\Container\ContainerInterface::get() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

61
            return $this->container->get(/** @scrutinizer ignore-type */ $middlewareDefinition);
Loading history...
62
        }
63
64 13
        if ($this->isCallableDefinition($middlewareDefinition)) {
65
            return $this->wrapCallableDefinition($middlewareDefinition);
0 ignored issues
show
Bug introduced by
It seems like $middlewareDefinition can also be of type string; however, parameter $callback of Yiisoft\Middleware\Dispa...rapCallableDefinition() does only seem to accept Closure|array, maybe add an additional type check? ( Ignorable by Annotation )

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

65
            return $this->wrapCallableDefinition(/** @scrutinizer ignore-type */ $middlewareDefinition);
Loading history...
66 13
        }
67 5
68
        if ($this->isArrayDefinition($middlewareDefinition)) {
69
            /**
70
             * @psalm-var ArrayDefinitionConfig $middlewareDefinition
71
             *
72
             * @var MiddlewareInterface
73
             */
74
            return ArrayDefinition::fromConfig($middlewareDefinition)->resolve($this->container);
0 ignored issues
show
Bug introduced by
It seems like $middlewareDefinition can also be of type callable and string; however, parameter $config of Yiisoft\Definitions\ArrayDefinition::fromConfig() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

74
            return ArrayDefinition::fromConfig(/** @scrutinizer ignore-type */ $middlewareDefinition)->resolve($this->container);
Loading history...
75 5
        }
76 5
77 5
        throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
78
    }
79
80
    /**
81
     * @param array|Closure $callback
82
     */
83
    private function wrapCallableDefinition($callback): MiddlewareInterface
84
    {
85 5
        if (is_array($callback)) {
86
            return new class ($this->container, $callback) implements MiddlewareInterface {
87
                private string $class;
88 5
                private string $method;
89 5
                private ContainerInterface $container;
90 5
                private array $callback;
91 4
92
                public function __construct(ContainerInterface $container, array $callback)
93
                {
94 1
                    [$this->class, $this->method] = $callback;
95
                    $this->container = $container;
96
                    $this->callback = $callback;
97
                }
98
99
                public function process(
100 1
                    ServerRequestInterface $request,
101
                    RequestHandlerInterface $handler
102
                ): ResponseInterface {
103
                    /** @var mixed $controller */
104
                    $controller = $this->container->get($this->class);
105
106 8
                    /** @var mixed $response */
107
                    $response = (new Injector($this->container))
108
                        ->invoke([$controller, $this->method], [$request, $handler]);
109
                    if ($response instanceof ResponseInterface) {
110
                        return $response;
111
                    }
112 8
113 8
                    throw new InvalidMiddlewareDefinitionException($this->callback);
114
                }
115
116
                public function __debugInfo()
117
                {
118
                    return [
119
                        'callback' => $this->callback,
120
                    ];
121 8
                }
122 8
            };
123 5
        }
124
125 3
        return new class ($callback, $this->container) implements MiddlewareInterface {
126 2
            private ContainerInterface $container;
127
            private $callback;
128 1
129
            public function __construct(callable $callback, ContainerInterface $container)
130
            {
131
                $this->callback = $callback;
132
                $this->container = $container;
133
            }
134
135
            public function process(
136
                ServerRequestInterface $request,
137
                RequestHandlerInterface $handler
138
            ): ResponseInterface {
139
                /** @var mixed $response */
140
                $response = (new Injector($this->container))->invoke($this->callback, [$request, $handler]);
141
                if ($response instanceof ResponseInterface) {
142
                    return $response;
143 23
                }
144
                if ($response instanceof MiddlewareInterface) {
145 23
                    return $response->process($request, $handler);
146 3
                }
147
                throw new InvalidMiddlewareDefinitionException($this->callback);
148
            }
149 21
        };
150 13
    }
151
152
    /**
153 8
     * @param mixed $definition
154
     *
155
     * @psalm-assert-if-true class-string<MiddlewareInterface> $definition
156
     */
157
    private function isMiddlewareClassDefinition($definition): bool
158
    {
159 21
        return is_string($definition)
160
            && is_subclass_of($definition, MiddlewareInterface::class);
161 21
    }
162 8
163
    /**
164
     * @param mixed $definition
165 13
     *
166 13
     * @psalm-assert-if-true array|Closure $definition
167 13
     */
168 13
    private function isCallableDefinition($definition): bool
169 6
    {
170 6
        if ($definition instanceof Closure) {
171 6
            return true;
172
        }
173
174
        return is_array($definition)
175
            && array_keys($definition) === [0, 1]
176
            && is_string($definition[0])
177
            && is_string($definition[1])
178
            && in_array(
179
                $definition[1],
180
                class_exists($definition[0]) ? get_class_methods($definition[0]) : [],
181
                true
182
            );
183
    }
184
185
    /**
186
     * @param mixed $definition
187
     *
188
     * @psalm-assert-if-true ArrayDefinitionConfig $definition
189
     */
190
    private function isArrayDefinition($definition): bool
191
    {
192
        if (!is_array($definition)) {
193
            return false;
194
        }
195
196
        try {
197
            DefinitionValidator::validateArrayDefinition($definition);
198
        } catch (InvalidConfigException $e) {
199
            return false;
200
        }
201
202
        return is_subclass_of((string) ($definition['class'] ?? ''), MiddlewareInterface::class);
203
    }
204
}
205