Completed
Push — master ( 09c050...8664f4 )
by Eric
10s
created

Router::uri()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 25
rs 8.439
cc 6
eloc 14
nc 5
nop 2
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\RouteParser\Std as Parser;
10
use FastRoute\RouteCollector;
11
use Jarvis\Jarvis;
12
use Symfony\Component\HttpFoundation\Response;
13
14
/**
15
 * @author Eric Chau <[email protected]>
16
 */
17
class Router extends Dispatcher
18
{
19
    private $computed = false;
20
    private $host = '';
21
    private $rawRoutes = [];
22
    private $routesNames = [];
23
    private $routeCollector;
24
    private $scheme = 'http';
25
26
    /**
27
     * Creates an instance of Router.
28
     *
29
     * Required to disable FastRoute\Dispatcher\GroupCountBased constructor.
30
     */
31
    public function __construct()
32
    {
33
    }
34
35
    /**
36
     * Adds a new route to the collection.
37
     *
38
     * We highly recommend you to use ::beginRoute() instead.
39
     * {@see ::beginRoute()}
40
     *
41
     * @param  Route $route
42
     * @return self
43
     */
44
    public function addRoute(Route $route): Router
45
    {
46
        $this->rawRoutes[] = [$route->method(), $route->pattern(), $route->handler()];
47
        $this->computed = false;
48
49
        if (null !== $name = $route->name()) {
50
            $this->routesNames[$name] = $route->pattern();
51
        }
52
53
        return $this;
54
    }
55
56
    /**
57
     * This is an helper that provides you a smooth syntax to add new route. Example:
58
     *
59
     * $router
60
     *     ->beginRoute('hello_world')
61
     *         ->setPattern('/hello/world')
62
     *         ->setHandler(function() {
63
     *             return 'Hello, world!';
64
     *         })
65
     *     ->end()
66
     * ;
67
     *
68
     * This syntax avoids you to create a new intance of Route, hydrating it and
69
     * then adding it to Router.
70
     *
71
     * @param  string|null $name
72
     * @return Route
73
     */
74
    public function beginRoute(string $name = null): Route
75
    {
76
        return new Route($name, $this);
77
    }
78
79
    /**
80
     * Generates and returns the full URL (with scheme and host) with provided URI.
81
     *
82
     * Notes that this method required at least the host to be setted.
83
     *
84
     * @param  string $uri
85
     * @return string
86
     */
87
    public function url(string $uri): string
88
    {
89
        $scheme = '';
90
        if ($this->host) {
91
            $uri = preg_replace('~/+~', '/', "{$this->host}$uri");
92
            $scheme = "{$this->scheme}://";
93
        }
94
95
        return "$scheme$uri";
96
    }
97
98
    /**
99
     * Returns the current scheme.
100
     *
101
     * @return string
102
     */
103
    public function scheme(): string
104
    {
105
        return $this->scheme;
106
    }
107
108
    /**
109
     * Sets the new scheme to use. Calling this method without parameter will reset
110
     * it to 'http'.
111
     *
112
     * @param string|null $scheme
113
     */
114
    public function setScheme(string $scheme = null): Router
115
    {
116
        $this->scheme = (string) $scheme ?: 'http';
117
118
        return $this;
119
    }
120
121
    /**
122
     * Returns the setted host.
123
     *
124
     * @return string
125
     */
126
    public function host(): string
127
    {
128
        return $this->host;
129
    }
130
131
    /**
132
     * Sets new host to Router. Calling this method without parameter will reset
133
     * the host to empty string.
134
     *
135
     * @param  string|null $host
136
     * @return self
137
     */
138
    public function setHost(string $host = null): Router
139
    {
140
        $this->host = (string) $host;
141
142
        return $this;
143
    }
144
145
    /**
146
     * Generates URI associated to provided route name.
147
     *
148
     * @param  string $name   The URI route name we want to generate
149
     * @param  array  $params Parameters to replace in pattern
150
     * @return string
151
     * @throws \InvalidArgumentException if provided route name is unknown
152
     */
153
    public function uri(string $name, array $params = []): string
154
    {
155
        if (!isset($this->routesNames[$name])) {
156
            throw new \InvalidArgumentException(
157
                "Cannot generate URI for '$name' cause it does not exist."
158
            );
159
        }
160
161
        $uri = $this->routesNames[$name];
162
        foreach ($params as $key => $value) {
163
            if (1 !== preg_match("~\{($key:?[^}]*)\}~", $uri, $matches)) {
164
                continue;
165
            }
166
167
            $value = (string) $value;
168
            $pieces = explode(':', $matches[1]);
169
            if (1 < count($pieces) && 1 !== preg_match("~{$pieces[1]}~", $value)) {
170
                continue;
171
            }
172
173
            $uri = str_replace($matches[0], $value, $uri);
174
        }
175
176
        return $uri;
177
    }
178
179
    /**
180
     * Matches the given HTTP method and URI to the route collection and returns
181
     * the callback with the array of arguments to use.
182
     *
183
     * @param  string $method
184
     * @param  string $uri
185
     * @return array
186
     */
187
    public function match(string $method, string $uri): array
188
    {
189
        $arguments = [];
190
        $callback = null;
191
        $result = $this->dispatch($method, $uri);
192
193
        if (Dispatcher::FOUND === $result[0]) {
194
            [1 => $callback, 2 => $arguments] = $result;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected '='
Loading history...
195
        } else {
196
            $callback = function() use ($result): Response {
197
                return new Response(null, Dispatcher::METHOD_NOT_ALLOWED === $result[0]
198
                    ? Response::HTTP_METHOD_NOT_ALLOWED
199
                    : Response::HTTP_NOT_FOUND
200
                );
201
            };
202
        }
203
204
        return [$callback, $arguments];
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     * Overrides GroupCountBased::dispatch() to ensure that dispatcher always deals with up-to-date
210
     * route collection.
211
     */
212
    public function dispatch($method, $uri): array
213
    {
214
        [$this->staticRouteMap, $this->variableRouteData] = $this->routeCollector()->getData();
215
216
        return parent::dispatch(strtolower($method), $uri);
217
    }
218
219
    /**
220
     * Will always return the right RouteCollector and knows when to recompute it.
221
     *
222
     * @return RouteCollector
223
     */
224
    private function routeCollector(): RouteCollector
225
    {
226
        if (!$this->computed) {
227
            $this->routeCollector = new RouteCollector(new Parser(), new DataGenerator());
228
229
            foreach ($this->rawRoutes as $rawRoute) {
230
                [$method, $route, $handler] = $rawRoute;
231
                $this->routeCollector->addRoute($method, $route, $handler);
232
            }
233
234
            $this->computed = true;
235
        }
236
237
        return $this->routeCollector;
238
    }
239
}
240