Completed
Push — master ( d8fbea...c06e4f )
by Arman
28s queued 12s
created

Router::findRoute()   B

Complexity

Conditions 8
Paths 33

Size

Total Lines 43
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 8
eloc 23
c 4
b 0
f 0
nc 33
nop 0
dl 0
loc 43
rs 8.4444
1
<?php
2
3
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 2.6.0
13
 */
14
15
namespace Quantum\Routes;
16
17
use Quantum\Exceptions\RouteException;
18
use Quantum\Debugger\Debugger;
19
use Quantum\Http\Response;
20
use Quantum\Http\Request;
21
use Psr\Log\LogLevel;
22
23
/**
24
 * Class Router
25
 * @package Quantum\Routes
26
 */
27
class Router extends RouteController
28
{
29
30
    /**
31
     * Request instance
32
     * @var \Quantum\Http\Request;
33
     */
34
    private $request;
35
36
    /**
37
     * Response instance
38
     * @var \Quantum\Http\Response;
39
     */
40
    private $response;
41
42
    /**
43
     * List of routes
44
     * @var array
45
     */
46
    private $routes = [];
47
48
    /**
49
     * matched routes
50
     * @var array
51
     */
52
    private $matchedRoutes = [];
53
54
    /**
55
     * Matched URI
56
     * @var string
57
     */
58
    private $matchedUri = null;
59
60
    /**
61
     * Router constructor.
62
     * @param \Quantum\Http\Request $request
63
     * @param \Quantum\Http\Response $response
64
     */
65
    public function __construct(Request $request, Response $response)
66
    {
67
        $this->request = $request;
68
        $this->response = $response;
69
    }
70
71
    /**
72
     * Finds the current route
73
     * @throws \Quantum\Exceptions\DiException
74
     * @throws \Quantum\Exceptions\RouteException
75
     * @throws \Quantum\Exceptions\StopExecutionException
76
     * @throws \ReflectionException
77
     */
78
    public function findRoute()
79
    {
80
        $this->resetRoutes();
81
82
        $uri = $this->request->getUri();
83
84
        if (!$uri) {
85
            throw RouteException::notFound();
86
        }
87
88
        $this->findStraightMatches($uri);
89
90
        if (!count($this->matchedRoutes)) {
91
            $this->findPatternMatches($uri);
92
        }
93
94
        if (!count($this->matchedRoutes)) {
95
            hook('errorPage');
96
            stop();
97
        }
98
99
        if (count($this->matchedRoutes) > 1) {
100
            $this->checkCollision();
101
        }
102
103
        $matchedRoute = current($this->matchedRoutes);
104
105
        hook('headers');
106
107
        if ($this->request->getMethod() != 'OPTIONS') {
108
            $this->checkMethod($matchedRoute);
109
        }
110
111
        $matchedRoute['uri'] = $this->request->getUri();
112
113
        self::setCurrentRoute($matchedRoute);
114
115
        if (filter_var(config()->get('debug'), FILTER_VALIDATE_BOOLEAN)) {
116
            $routeInfo = [];
117
            array_walk($matchedRoute, function ($value, $key) use (&$routeInfo) {
118
                $routeInfo[ucfirst($key)] = is_array($value) ? implode(', ', $value) : $value;
119
            });
120
            Debugger::addToStore(Debugger::ROUTES, LogLevel::INFO, $routeInfo);
121
        }
122
123
    }
124
125
    /**
126
     * Set Routes
127
     * @param array $routes
128
     */
129
    public function setRoutes(array $routes)
130
    {
131
        $this->routes = $routes;
132
    }
133
134
    /**
135
     * Get Routes
136
     * @return array
137
     */
138
    public function getRoutes(): array
139
    {
140
        return $this->routes;
141
    }
142
143
    /**
144
     * Resets the routes
145
     */
146
    private function resetRoutes()
147
    {
148
        parent::$currentRoute = null;
149
        $this->matchedUri = null;
150
        $this->matchedRoutes = [];
151
    }
152
153
    /**
154
     * Finds straight matches
155
     * @param string $uri
156
     */
157
    private function findStraightMatches(string $uri)
158
    {
159
        $requestUri = trim(urldecode(preg_replace('/[?]/', '', $uri)), '/');
160
161
        foreach ($this->routes as $route) {
162
            if ($requestUri == trim($route['route'], '/')) {
163
                $route['args'] = [];
164
                $this->matchedUri = $route['route'];
165
                $this->matchedRoutes[] = $route;
166
            }
167
        }
168
    }
169
170
    /**
171
     * Finds matches by pattern
172
     * @param string $uri
173
     */
174
    private function findPatternMatches(string $uri)
175
    {
176
        $requestUri = urldecode(parse_url($uri)['path']);
177
178
        foreach ($this->routes as $route) {
179
            $pattern = trim($route['route'], '/');
180
            $pattern = str_replace('/', '\/', $pattern);
181
            $pattern = preg_replace_callback('/(\\\\\/)*\[(:num)(:([0-9]+))*\](\?)?/', [$this, 'getPattern'], $pattern);
182
            $pattern = preg_replace_callback('/(\\\\\/)*\[(:alpha)(:([0-9]+))*\](\?)?/', [$this, 'getPattern'], $pattern);
183
            $pattern = preg_replace_callback('/(\\\\\/)*\[(:any)(:([0-9]+))*\](\?)?/', [$this, 'getPattern'], $pattern);
184
185
            $pattern = mb_substr($pattern, 0, 4) != '(\/)' ? '(\/)?' . $pattern : $pattern;
186
187
            preg_match("/^" . $pattern . "$/u", $requestUri, $matches);
188
189
            if (count($matches)) {
190
                $this->matchedUri = reset($matches) ?: '/';
191
                array_shift($matches);
192
                $route['args'] = array_diff($matches, ['', '/']);
193
                $route['pattern'] = $pattern;
194
                $this->matchedRoutes[] = $route;
195
            }
196
        }
197
    }
198
199
    /**
200
     * Checks the route collisions
201
     * @throws \Quantum\Exceptions\RouteException
202
     */
203
    private function checkCollision()
204
    {
205
        $length = count($this->matchedRoutes);
206
207
        for ($i = 0; $i < $length - 1; $i++) {
208
            for ($j = $i + 1; $j < $length; $j++) {
209
                if ($this->matchedRoutes[$i]['method'] == $this->matchedRoutes[$j]['method']) {
210
                    throw RouteException::repetitiveRouteSameMethod($this->matchedRoutes[$j]['method']);
211
212
                }
213
                if ($this->matchedRoutes[$i]['module'] != $this->matchedRoutes[$j]['module']) {
214
                    throw RouteException::repetitiveRouteDifferentModules();
215
                }
216
            }
217
        }
218
    }
219
220
    /**
221
     * Checks the request method against defined route method
222
     * @param array $matchedRoute
223
     * @throws \Quantum\Exceptions\RouteException
224
     */
225
    private function checkMethod(array $matchedRoute)
226
    {
227
        if (strpos($matchedRoute['method'], '|') !== false) {
228
            if (!in_array($this->request->getMethod(), explode('|', $matchedRoute['method']))) {
229
                throw RouteException::incorrectMethod($this->request->getMethod());
230
            }
231
        } else if ($this->request->getMethod() != $matchedRoute['method']) {
232
            throw RouteException::incorrectMethod($this->request->getMethod());
233
        }
234
    }
235
236
    /**
237
     * Finds URL pattern
238
     * @param array $matches
239
     * @return string
240
     */
241
    private function getPattern(array $matches): string
242
    {
243
        $replacement = '';
244
245
        if (isset($matches[5]) && $matches[5] == '?') {
246
            $replacement .= '(\/)?';
247
        } else {
248
            $replacement .= '(\/)';
249
        }
250
251
        switch ($matches[2]) {
252
            case ':num':
253
                $replacement .= '([0-9]';
254
                break;
255
            case ':alpha':
256
                $replacement .= '([a-zA-Z]';
257
                break;
258
            case ':any':
259
                $replacement .= '([^\/]';
260
                break;
261
        }
262
263
        if (isset($matches[4]) && is_numeric($matches[4])) {
264
            if (isset($matches[5]) && $matches[5] == '?') {
265
                $replacement .= '{0,' . $matches[4] . '})';
266
            } else {
267
                $replacement .= '{' . $matches[4] . '})';
268
            }
269
        } else {
270
            if (isset($matches[5]) && $matches[5] == '?') {
271
                $replacement .= '*)';
272
            } else {
273
                $replacement .= '+)';
274
            }
275
        }
276
277
        return $replacement;
278
    }
279
280
}
281