Completed
Push — master ( 8d6a53...3eaa43 )
by Eric
03:21 queued 55s
created

Router   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Importance

Changes 0
Metric Value
wmc 26
lcom 2
cbo 7
dl 0
loc 243
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A addRoute() 0 11 2
A beginRoute() 0 4 1
A url() 0 10 2
A scheme() 0 4 1
A setScheme() 0 6 2
A host() 0 4 1
A setHost() 0 6 1
A guessHost() 0 10 2
B uri() 0 25 6
A match() 0 19 3
A dispatch() 0 6 1
A routeCollector() 0 14 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Jarvis\Skill\Routing;
6
7
use FastRoute\DataGenerator\GroupCountBased as DataGenerator;
8
use FastRoute\Dispatcher\GroupCountBased as Dispatcher;
9
use FastRoute\RouteCollector;
10
use FastRoute\RouteParser\Std as Parser;
11
use Jarvis\Jarvis;
12
use Symfony\Component\HttpFoundation\Request;
13
use Symfony\Component\HttpFoundation\Response;
14
15
/**
16
 * @author Eric Chau <[email protected]>
17
 */
18
class Router extends Dispatcher
19
{
20
    const DEFAULT_SCHEME = 'http';
21
    const HTTP_PORT = 80;
22
    const HTTPS_PORT = 443;
23
24
    private $computed = false;
25
    private $host = '';
26
    private $rawRoutes = [];
27
    private $routesNames = [];
28
    private $routeCollector;
29
    private $scheme = self::DEFAULT_SCHEME;
30
31
    /**
32
     * Creates an instance of Router.
33
     *
34
     * Required to disable FastRoute\Dispatcher\GroupCountBased constructor.
35
     */
36
    public function __construct()
37
    {
38
    }
39
40
    /**
41
     * Adds a new route to the collection.
42
     *
43
     * We highly recommend you to use ::beginRoute() instead.
44
     * {@see ::beginRoute()}
45
     *
46
     * @param  Route $route
47
     * @return self
48
     */
49
    public function addRoute(Route $route): Router
50
    {
51
        $this->rawRoutes[] = [$route->method(), $route->pattern(), $route->handler()];
52
        $this->computed = false;
53
54
        if (false != $name = $route->name()) {
55
            $this->routesNames[$name] = $route->pattern();
56
        }
57
58
        return $this;
59
    }
60
61
    /**
62
     * This is an helper that provides you a smooth syntax to add new route. Example:
63
     *
64
     * $router
65
     *     ->beginRoute('hello_world')
66
     *         ->setPattern('/hello/world')
67
     *         ->setHandler(function () {
68
     *             return 'Hello, world!';
69
     *         })
70
     *     ->end()
71
     * ;
72
     *
73
     * This syntax avoids you to create a new intance of Route, hydrating it and
74
     * then adding it to Router.
75
     *
76
     * @param  string|null $name
77
     * @return Route
78
     */
79
    public function beginRoute(string $name = null): Route
80
    {
81
        return new Route($this, $name);
82
    }
83
84
    /**
85
     * Generates and returns the full URL (with scheme and host) with provided URI.
86
     *
87
     * Notes that this method required at least the host to be setted.
88
     *
89
     * @param  string $uri
90
     * @return string
91
     */
92
    public function url(string $uri): string
93
    {
94
        $scheme = '';
95
        if ($this->host) {
96
            $uri = preg_replace('~/+~', '/', "{$this->host}$uri");
97
            $scheme = "{$this->scheme}://";
98
        }
99
100
        return "$scheme$uri";
101
    }
102
103
    /**
104
     * Returns the current scheme.
105
     *
106
     * @return string
107
     */
108
    public function scheme(): string
109
    {
110
        return $this->scheme;
111
    }
112
113
    /**
114
     * Sets the new scheme to use. Calling this method without parameter will reset
115
     * it to 'http'.
116
     *
117
     * @param string|null $scheme
118
     */
119
    public function setScheme(string $scheme = null): Router
120
    {
121
        $this->scheme = (string) $scheme ?: self::DEFAULT_SCHEME;
122
123
        return $this;
124
    }
125
126
    /**
127
     * Returns the setted host.
128
     *
129
     * @return string
130
     */
131
    public function host(): string
132
    {
133
        return $this->host;
134
    }
135
136
    /**
137
     * Sets new host to Router. Calling this method without parameter will reset
138
     * the host to empty string.
139
     *
140
     * @param  string|null $host
141
     * @return self
142
     */
143
    public function setHost(string $host = null): Router
144
    {
145
        $this->host = (string) $host;
146
147
        return $this;
148
    }
149
150
    /**
151
     * Uses the provided request to guess the host. This method also set the
152
     *
153
     * @param  Request $request
154
     * @return self
155
     */
156
    public function guessHost(Request $request)
157
    {
158
        $this->setScheme($request->getScheme());
159
        $this->setHost($request->getHost());
160
        if (!in_array($request->getPort(), [self::HTTP_PORT, self::HTTPS_PORT])) {
161
            $this->setHost($this->host() . ':' . $request->getPort());
162
        }
163
164
        return $this;
165
    }
166
167
    /**
168
     * Generates URI associated to provided route name.
169
     *
170
     * @param  string $name   The URI route name we want to generate
171
     * @param  array  $params Parameters to replace in pattern
172
     * @return string
173
     * @throws \InvalidArgumentException if provided route name is unknown
174
     */
175
    public function uri(string $name, array $params = []): string
176
    {
177
        if (!isset($this->routesNames[$name])) {
178
            throw new \InvalidArgumentException(
179
                "Cannot generate URI for '$name' cause it does not exist."
180
            );
181
        }
182
183
        $uri = $this->routesNames[$name];
184
        foreach ($params as $key => $value) {
185
            if (1 !== preg_match("~\{($key:?[^}]*)\}~", $uri, $matches)) {
186
                continue;
187
            }
188
189
            $value = (string) $value;
190
            $pieces = explode(':', $matches[1]);
191
            if (1 < count($pieces) && 1 !== preg_match("~{$pieces[1]}~", $value)) {
192
                continue;
193
            }
194
195
            $uri = str_replace($matches[0], $value, $uri);
196
        }
197
198
        return $uri;
199
    }
200
201
    /**
202
     * Matches the given HTTP method and URI to the route collection and returns
203
     * the callback with the array of arguments to use.
204
     *
205
     * @param  string $method
206
     * @param  string $uri
207
     * @return array
208
     */
209
    public function match(string $method, string $uri): array
210
    {
211
        $arguments = [];
212
        $callback = null;
213
        $result = $this->dispatch($method, $uri);
214
215
        if (Dispatcher::FOUND === $result[0]) {
216
            [1 => $callback, 2 => $arguments] = $result;
217
        } else {
218
            $callback = function () use ($result): Response {
219
                return new Response(null, Dispatcher::NOT_FOUND === $result[0]
220
                    ? Response::HTTP_NOT_FOUND
221
                    : Response::HTTP_METHOD_NOT_ALLOWED
222
                );
223
            };
224
        }
225
226
        return [$callback, $arguments];
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     * Overrides GroupCountBased::dispatch() to ensure that dispatcher always deals with up-to-date
232
     * route collection.
233
     */
234
    public function dispatch($method, $uri): array
235
    {
236
        [$this->staticRouteMap, $this->variableRouteData] = $this->routeCollector()->getData();
237
238
        return parent::dispatch(strtolower($method), $uri);
239
    }
240
241
    /**
242
     * Will always return the right RouteCollector and knows when to recompute it.
243
     *
244
     * @return RouteCollector
245
     */
246
    private function routeCollector(): RouteCollector
247
    {
248
        if (!$this->computed) {
249
            $this->routeCollector = new RouteCollector(new Parser(), new DataGenerator());
250
            foreach ($this->rawRoutes as $rawRoute) {
251
                [$method, $route, $handler] = $rawRoute;
0 ignored issues
show
Bug introduced by
The variable $method does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $route does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $handler does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
252
                $this->routeCollector->addRoute($method, $route, $handler);
253
            }
254
255
            $this->computed = true;
256
        }
257
258
        return $this->routeCollector;
259
    }
260
}
261