RouterEngine::route()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
cc 3
eloc 10
nc 3
nop 2
dl 0
loc 21
rs 9.9332
c 7
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Scrawler package.
7
 *
8
 * (c) Pranjal Pandey <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Scrawler\Router;
15
16
/**
17
 * This class routes the URL to corrosponding controller.
18
 */
19
final class RouterEngine
20
{
21
    // ---------------------------------------------------------------//
22
23
    /**
24
     * Stores the URL broken logic wise.
25
     *
26
     * @var array<int,string>
27
     */
28
    private array $pathInfo = [];
29
30
    /**
31
     * Stores the request method i.e get,post etc.
32
     */
33
    private string $httpMethod;
34
35
    /**
36
     * Stores the request uri.
37
     */
38
    private string $uri;
39
40
    /**
41
     * Stores dir mode.
42
     */
43
    private bool $dirMode = false;
44
45
    /**
46
     * Store Dirctory during dir Mode.
47
     */
48
    private string $dir = '';
49
50
    /**
51
     * stores debug msg.
52
     */
53
    private string $debugMsg = '';
54
55
    // ---------------------------------------------------------------//
56
57
    /**
58
     * constructor overloading for auto routing.
59
     */
60
    public function __construct(
61
        /**
62
         * Stores the RouterCollection object.
63
         */
64
        private readonly RouteCollection $collection,
65
    ) {
66
    }
67
68
    // ---------------------------------------------------------------//
69
70
    /**
71
     * Detects the URL and call the corrosponding method
72
     * of corrosponding controller.
73
     *
74
     * @return array<int, mixed>
75
     */
76
    public function route(string $httpMethod, string $uri): array
77
    {
78
        $this->httpMethod = strtolower($httpMethod);
79
        $this->uri = $uri;
80
81
        // Break URL into segments
82
        $this->pathInfo = explode('/', $uri);
83
        array_shift($this->pathInfo);
84
85
        // Try manual routing
86
        [$status, $handler, $args] = $this->routeManual();
87
        if ($status) {
88
            return [1, $handler, $args, ''];
89
        }
90
91
        // Try auto routing
92
        if ($this->collection->isAutoRegistered()) {
93
            return $this->routeAuto();
94
        }
95
96
        return [0, '', [], $this->debugMsg];
97
    }
98
99
    /**
100
     * Set Arguments on the request object.
101
     *
102
     * @return array<int, mixed>
103
     */
104
    private function routeAuto(): array
105
    {
106
        $controller = $this->getController();
107
        $method = $this->getMethod($controller);
108
        if ('' === $method) {
109
            if ($this->checkMethodNotAllowed($controller)) {
110
                return [2, '', [], $this->debugMsg];
111
            }
112
113
            return [0, '', [], $this->debugMsg];
114
        }
115
        $handler = $controller.'::'.$method;
116
        $arguments = $this->getArguments($controller, $method);
117
118
        if (is_bool($arguments) && !$arguments) {
0 ignored issues
show
introduced by
The condition is_bool($arguments) is always false.
Loading history...
119
            return [0, '', [], $this->debugMsg];
120
        }
121
122
        return [1, $handler, $arguments, ''];
123
    }
124
125
    /**
126
     * Function to get namespace.
127
     */
128
    private function getNamespace(): string
129
    {
130
        if ($this->dirMode) {
131
            return $this->collection->getNamespace().'\\'.$this->dir;
132
        }
133
134
        return $this->collection->getNamespace();
135
    }
136
137
    // ---------------------------------------------------------------//
138
139
    /**
140
     * Function to get controller.
141
     */
142
    private function getController(): string
143
    {
144
        $controller = ucfirst($this->pathInfo[0]);
145
146
        if (isset($this->pathInfo[0]) && $this->collection->isDir(ucfirst($this->pathInfo[0]))) {
147
            $this->dir = ucfirst($this->pathInfo[0]);
148
            $this->dirMode = true;
149
            array_shift($this->pathInfo);
150
        }
151
152
        if ($this->dirMode && isset($this->pathInfo[0])) {
153
            $controller = $this->dir.'/'.ucfirst($this->pathInfo[0]);
154
        }
155
156
        // Set corrosponding controller
157
        if (isset($this->pathInfo[0]) && '' !== $this->pathInfo[0] && '0' !== $this->pathInfo[0]) {
158
            $controller = $this->collection->getController($controller);
159
        } else {
160
            $controller = $this->getNamespace().'\Main';
161
        }
162
163
        if (!$controller) {
164
            $controller = '';
165
        }
166
167
        if (class_exists($controller)) {
168
            return $controller;
169
        }
170
171
        $controller = $this->getNamespace().'\Main';
172
173
        if (class_exists($controller)) {
174
            array_unshift($this->pathInfo, '');
175
176
            return $controller;
177
        }
178
179
        $this->debug('No Controller could be resolved:'.$controller);
180
181
        return '';
182
    }
183
184
    /**
185
     * Function to throw 404 error.
186
     */
187
    private function debug(string $message): void
188
    {
189
        $this->debugMsg = $message;
190
    }
191
192
    // ---------------------------------------------------------------//
193
194
    /**
195
     * Function to dispach the method if method exist.
196
     *
197
     * @return bool|array<mixed>
198
     */
199
    private function getArguments(string $controller, string $method): bool|array
200
    {
201
        $controllerObj = new $controller();
202
203
        $arguments = [];
204
        $counter = count($this->pathInfo);
205
        for ($j = 2; $j < $counter; ++$j) {
206
            $arguments[] = $this->pathInfo[$j];
207
        }
208
        // Check weather arguments are passed else throw a 404 error
209
        $classMethod = new \ReflectionMethod($controllerObj, $method);
210
        $params = $classMethod->getParameters();
211
        // Remove params if it allows null
212
        foreach ($params as $key => $param) {
213
            if ($param->isOptional()) {
214
                unset($params[$key]);
215
            }
216
        }
217
        if (count($arguments) < count($params)) {
218
            $this->debug('Not enough arguments given to the method');
219
220
            return false;
221
        }
222
        // finally fix the long awaited allIndex bug !
223
        if (count($arguments) > count($classMethod->getParameters())) {
224
            $this->debug('Not able to resolve '.$method.'for'.$controller.'controller');
225
226
            return false;
227
        }
228
229
        return $arguments;
230
    }
231
232
    /**
233
     * Function to check for 405.
234
     */
235
    private function checkMethodNotAllowed(string $controller): bool
236
    {
237
        if (!isset($this->pathInfo[1])) {
238
            return false;
239
        }
240
241
        return method_exists($controller, 'get'.ucfirst($this->pathInfo[1])) || method_exists($controller, 'post'.ucfirst($this->pathInfo[1])) || method_exists($controller, 'put'.ucfirst($this->pathInfo[1])) || method_exists($controller, 'delete'.ucfirst($this->pathInfo[1]));
242
    }
243
244
    /**
245
     * Returns the method to be called according to URL.
246
     */
247
    private function getMethod(string $controller): string
248
    {
249
        // Set Method from second argument from URL
250
        if (isset($this->pathInfo[1])) {
251
            if (method_exists($controller, $function = $this->httpMethod.ucfirst($this->pathInfo[1]))) {
252
                return $function;
253
            }
254
            if (method_exists($controller, $function = 'all'.ucfirst($this->pathInfo[1]))) {
255
                return $function;
256
            }
257
        }
258
259
        if (isset($function)) {
260
            $last_function = $function;
261
        }
262
        if (method_exists($controller, $function = $this->httpMethod.'Index')) {
263
            array_unshift($this->pathInfo, '');
264
265
            return $function;
266
        }
267
        // Last attempt to invoke allIndex
268
        if (method_exists($controller, $function = 'allIndex')) {
269
            array_unshift($this->pathInfo, '');
270
271
            return $function;
272
        }
273
274
        if (isset($last_function)) {
275
            $this->debug('Neither '.$function.' method nor '.$last_function.' method you found in '.$controller.' controller');
276
277
            return '';
278
        }
279
280
        $this->debug($function.' method not found in '.$controller.' controller');
281
282
        return '';
283
    }
284
285
    /**
286
     * Function to route manual routes.
287
     *
288
     * @return array<int, mixed>
289
     */
290
    private function routeManual(): array
291
    {
292
        $controller = null;
293
        $arguments = [];
294
        $routes = $this->collection->getRoutes();
295
        $collection_route = $this->collection->getRoute($this->uri, $this->httpMethod);
296
        if ($collection_route) {
297
            $controller = $collection_route;
298
        } elseif ([] !== $routes) {
299
            $tokens = [
300
                ':string' => '([a-zA-Z]+)',
301
                ':number' => '(\d+)',
302
                ':alpha' => '([a-zA-Z0-9-_]+)',
303
            ];
304
305
            foreach ($routes[$this->httpMethod] as $pattern => $handler_name) {
306
                $pattern = strtr($pattern, $tokens);
307
                if (0 !== \Safe\preg_match('#^/?'.$pattern.'/?$#', $this->uri, $matches)) {
308
                    $controller = $handler_name;
309
                    $arguments = $matches;
310
                    break;
311
                }
312
            }
313
        }
314
315
        if (is_callable($controller)) {
316
            unset($arguments[0]);
317
318
            return [true, $controller, $arguments];
319
        }
320
321
        return [false, '', ''];
322
    }
323
}
324