Passed
Pull Request — master (#68)
by Dmitriy
12:58
created

Dispatcher::isCallable()

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router;
6
7
use InvalidArgumentException;
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
final class Dispatcher implements DispatcherInterface
16
{
17
    /**
18
     * Contains a stack of middleware wrapped in handlers.
19
     * Each handler points to the handler of middleware that will be processed next.
20
     * @var RequestHandlerInterface|null stack of middleware
21
     */
22
    private ?RequestHandlerInterface $stack = null;
23
24
    private ContainerInterface $container;
25
    /**
26
     * @var callable[]|string[]|array[]
27
     */
28
    private array $middlewares = [];
29
30
    public function __construct(ContainerInterface $container)
31
    {
32
        $this->container = $container;
33
    }
34
35
    public function dispatch(ServerRequestInterface $request, RequestHandlerInterface $fallbackHandler): ResponseInterface
36
    {
37
        $handler = $fallbackHandler;
38
        if ($this->stack === null) {
39
            foreach ($this->middlewares as $middleware) {
40
                $handler = $this->wrap($this->prepareMiddleware($middleware), $handler);
41
            }
42
            $this->stack = $handler;
43
        }
44
45
        return $this->stack->handle($request);
0 ignored issues
show
Bug introduced by
The method handle() does not exist on null. ( Ignorable by Annotation )

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

45
        return $this->stack->/** @scrutinizer ignore-call */ handle($request);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
46
    }
47
48
    public function withMiddlewares(array $middlewares): DispatcherInterface
49
    {
50
        $clone = clone $this;
51
        $clone->middlewares = $middlewares;
52
        $clone->stack = null;
53
54
        return $clone;
55
    }
56
57
    public function hasMiddlewares(): bool
58
    {
59
        return $this->middlewares !== [];
60
    }
61
62
    /**
63
     * Wraps handler by middlewares
64
     */
65
    private function wrap(MiddlewareInterface $middleware, RequestHandlerInterface $handler): RequestHandlerInterface
66
    {
67
        return new class($middleware, $handler) implements RequestHandlerInterface {
68
            private MiddlewareInterface $middleware;
69
            private RequestHandlerInterface $handler;
70
71
            public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $handler)
72
            {
73
                $this->middleware = $middleware;
74
                $this->handler = $handler;
75
            }
76
77
            public function handle(ServerRequestInterface $request): ResponseInterface
78
            {
79
                return $this->middleware->process($request, $this->handler);
80
            }
81
        };
82
    }
83
84
    private function wrapCallable($callback): MiddlewareInterface
85
    {
86
        if (is_array($callback) && !is_object($callback[0])) {
87
            [$controller, $action] = $callback;
88
            return new class($controller, $action, $this->container) implements MiddlewareInterface {
89
                private string $class;
90
                private string $method;
91
                private ContainerInterface $container;
92
93
                public function __construct(string $class, string $method, ContainerInterface $container)
94
                {
95
                    $this->class = $class;
96
                    $this->method = $method;
97
                    $this->container = $container;
98
                }
99
100
                public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
101
                {
102
                    $controller = $this->container->get($this->class);
103
                    return (new Injector($this->container))->invoke([$controller, $this->method], [$request, $handler]);
104
                }
105
            };
106
        }
107
108
        return new class($callback, $this->container) implements MiddlewareInterface {
109
            private ContainerInterface $container;
110
            private $callback;
111
112
            public function __construct(callable $callback, ContainerInterface $container)
113
            {
114
                $this->callback = $callback;
115
                $this->container = $container;
116
            }
117
118
            public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
119
            {
120
                $response = (new Injector($this->container))->invoke($this->callback, [$request, $handler]);
121
                return $response instanceof MiddlewareInterface ? $response->process($request, $handler) : $response;
122
            }
123
        };
124
    }
125
126
    /**
127
     * @param callable|string|array $middleware
128
     * @return MiddlewareInterface|string|array
129
     */
130
    private function prepareMiddleware($middleware)
131
    {
132
        if (is_string($middleware)) {
133
            if ($this->container === null) {
134
                throw new InvalidArgumentException('Route container must not be null for lazy loaded middleware.');
135
            }
136
            return $this->container->get($middleware);
137
        }
138
139
        if (is_array($middleware) && !is_object($middleware[0])) {
140
            if ($this->container === null) {
141
                throw new InvalidArgumentException('Route container must not be null for handler action.');
142
            }
143
            return $this->wrapCallable($middleware);
144
        }
145
146
        if ($this->isCallable($middleware)) {
147
            if ($this->container === null) {
148
                throw new InvalidArgumentException('Route container must not be null for callable.');
149
            }
150
            return $this->wrapCallable($middleware);
151
        }
152
153
        return $middleware;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $middleware returns the type callable which is incompatible with the documented return type Psr\Http\Server\MiddlewareInterface|array|string.
Loading history...
154
    }
155
156
    private function isCallable($definition): bool
157
    {
158
        if (is_callable($definition)) {
159
            return true;
160
        }
161
162
        return is_array($definition) && array_keys($definition) === [0, 1] && in_array($definition[1], get_class_methods($definition[0]) ?? [], true);
163
    }
164
}
165