Completed
Branch 4.0 (9dec83)
by Marc André
03:29
created

Router::generateCache()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 16
rs 9.2
cc 4
eloc 10
nc 2
nop 1
1
<?php
2
3
4
/**
5
 *
6
 * Copyright (c) 2010-2017 Nevraxe inc. & Marc André Audet <[email protected]>. All rights reserved.
7
 *
8
 * Redistribution and use in source and binary forms, with or without modification, are
9
 * permitted provided that the following conditions are met:
10
 *
11
 *   1. Redistributions of source code must retain the above copyright notice, this list of
12
 *       conditions and the following disclaimer.
13
 *
14
 *   2. Redistributions in binary form must reproduce the above copyright notice, this list
15
 *       of conditions and the following disclaimer in the documentation and/or other materials
16
 *       provided with the distribution.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL MARC ANDRÉ "MANHIM" AUDET BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 *
29
 */
30
31
32
namespace Cervo\Libraries;
33
34
35
use Cervo\Core as _;
36
use Cervo\Exceptions\InvalidMiddlewareException;
37
use Cervo\Exceptions\InvalidRouterCacheException;
38
use Cervo\Exceptions\MethodNotAllowedException;
39
use Cervo\Exceptions\RouteMiddlewareFailedException;
40
use Cervo\Exceptions\RouteNotFoundException;
41
use Cervo\Route;
42
use FastRoute\RouteCollector;
43
use FastRoute\RouteParser as RouteParser;
44
use FastRoute\DataGenerator as DataGenerator;
45
use FastRoute\Dispatcher as Dispatcher;
46
47
48
/**
49
 * Route manager for Cervo.
50
 *
51
 * @author Marc André Audet <[email protected]>
52
 */
53
final class Router
54
{
55
    /**
56
     * FastRoute, null if usingCache is set
57
     * @var RouteCollector
58
     */
59
    private $routeCollector = null;
60
61
    /**
62
     * FastRoute cache file path.
63
     * @var string
64
     */
65
    private $cacheFilePath;
66
67
    /**
68
     * List of middlewares called using the middleware() method.
69
     * @var array
70
     */
71
    private $currentMiddlewares = [];
72
73
    /**
74
     * List of group prefixes called using the group() method.
75
     * @var string
76
     */
77
    private $currentGroupPrefix;
78
79
    /**
80
     * Initialize the route configurations.
81
     */
82
    public function __construct()
83
    {
84
        $config = _::getLibrary('Cervo/Config');
85
86
        $this->cacheFilePath = $config->get('Cervo/Application/Directory') . \DS . 'router.cache.php';
87
88
        $this->routeCollector = new RouteCollector(
89
            new RouteParser\Std(),
90
            new DataGenerator\GroupCountBased()
91
        );
92
93
        foreach (glob($config->get('Cervo/Application/Directory') . '*' . \DS . 'Router.php', \GLOB_NOSORT | \GLOB_NOESCAPE) as $file) {
94
95
            $function = require $file;
96
97
            if (is_callable($function)) {
98
                $function($this);
99
            }
100
101
        }
102
    }
103
104
    /**
105
     * Encapsulate all the routes that are added from $func(Router) with this middleware.
106
     *
107
     * If the return value of the middleware is false, throws a RouteMiddlewareFailedException.
108
     *
109
     * @param string $library_name The library to call through \Cervo\Core::getLibrary( string )
110
     * @param string $method_name The method to call through the library
111
     * @param callable $func
112
     */
113
    public function middleware(string $library_name, string $method_name, callable $func) : void
114
    {
115
        // It's easier to cache an array
116
        array_push($this->currentMiddlewares, [
117
            'library' => $library_name,
118
            'method' => $method_name
119
        ]);
120
121
        $func($this);
122
123
        array_pop($this->currentMiddlewares);
124
    }
125
126
    /**
127
     * Adds a prefix in front of all the encapsulated routes.
128
     *
129
     * @param string $prefix The prefix of the group.
130
     * @param callable $func
131
     */
132
    public function group(string $prefix, callable $func) : void
133
    {
134
        $previousGroupPrefix = $this->currentGroupPrefix;
135
        $this->currentGroupPrefix = $previousGroupPrefix . $prefix;
136
137
        $func($this);
138
139
        $this->currentGroupPrefix = $previousGroupPrefix;
140
    }
141
142
    /**
143
     * Dispatch the request to the router.
144
     *
145
     * @return Route
146
     * @throws MethodNotAllowedException if the request method is not supported, but others are for this route.
147
     * @throws RouteNotFoundException if the requested route did not match any routes.
148
     */
149
    public function dispatch() : Route
150
    {
151
        $dispatcher = $this->getDispatcher();
152
153
        if (defined('STDIN')) {
154
            $request_method = 'CLI';
155
        } else {
156
            $request_method = $_SERVER['REQUEST_METHOD'];
157
        }
158
159
        $routeInfo = $dispatcher->dispatch($request_method, $this->detectUri());
160
161
        if ($routeInfo[0] === Dispatcher::FOUND) {
162
163
            $handler = $routeInfo[1];
164
            $arguments = $routeInfo[2];
165
            $middlewares = $handler['middlewares'];
166
167
            if (is_array($middlewares)) {
168
                $this->handleMiddlewares($middlewares, $handler['parameters'], $arguments);
169
            }
170
171
            return new Route($handler['method_path'], $handler['parameters'], $arguments);
172
173
        } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {
174
            throw new MethodNotAllowedException($routeInfo[1]);
175
        } else {
176
            throw new RouteNotFoundException;
177
        }
178
    }
179
180
    /**
181
     * Add a new route.
182
     *
183
     * @param string|string[] $http_method The HTTP method, example: GET, POST, PATCH, PUT, DELETE, CLI, etc. Can be an array of values.
184
     * @param string $route The route
185
     * @param string $method_path The Method Path
186
     * @param array $parameters The parameters to pass
187
     */
188
    public function addRoute($http_method, string $route, string $method_path, array $parameters = []) : void
189
    {
190
        if (_::getLibrary('Cervo/Config')->get('Production') == true && file_exists($this->cacheFilePath)) {
191
            return;
192
        }
193
194
        $route = $this->currentGroupPrefix . $route;
195
196
        $this->routeCollector->addRoute($http_method, $route, [
197
            'method_path' => $method_path,
198
            'middlewares' => $this->currentMiddlewares,
199
            'parameters' => $parameters
200
        ]);
201
    }
202
203
    /**
204
     * Add a new route with GET as HTTP method.
205
     *
206
     * @param string $route The route
207
     * @param string $method_path The Method Path
208
     * @param array $parameters The parameters to pass
209
     */
210
    public function get(string $route, string $method_path, array $parameters = []) : void
211
    {
212
        $this->addRoute('GET', $route, $method_path, $parameters);
213
    }
214
215
    /**
216
     * Add a new route with POST as HTTP method.
217
     *
218
     * @param string $route The route
219
     * @param string $method_path The Method Path
220
     * @param array $parameters The parameters to pass
221
     */
222
    public function post(string $route, string $method_path, array $parameters = []) : void
223
    {
224
        $this->addRoute('POST', $route, $method_path, $parameters);
225
    }
226
227
    /**
228
     * Add a new route with PUT as HTTP method.
229
     *
230
     * @param string $route The route
231
     * @param string $method_path The Method Path
232
     * @param array $parameters The parameters to pass
233
     */
234
    public function put(string $route, string $method_path, array $parameters = []) : void
235
    {
236
        $this->addRoute('PUT', $route, $method_path, $parameters);
237
    }
238
239
    /**
240
     * Add a new route with PATCH as HTTP method.
241
     *
242
     * @param string $route The route
243
     * @param string $method_path The Method Path
244
     * @param array $parameters The parameters to pass
245
     */
246
    public function patch(string $route, string $method_path, array $parameters = []) : void
247
    {
248
        $this->addRoute('PATCH', $route, $method_path, $parameters);
249
    }
250
251
    /**
252
     * Add a new route with DELETE as HTTP method.
253
     *
254
     * @param string $route The route
255
     * @param string $method_path The Method Path
256
     * @param array $parameters The parameters to pass
257
     */
258
    public function delete(string $route, string $method_path, array $parameters = []) : void
259
    {
260
        $this->addRoute('DELETE', $route, $method_path, $parameters);
261
    }
262
263
    /**
264
     * Add a new route with HEAD as HTTP method.
265
     *
266
     * @param string $route The route
267
     * @param string $method_path The Method Path
268
     * @param array $parameters The parameters to pass
269
     */
270
    public function head(string $route, string $method_path, array $parameters = []) : void
271
    {
272
        $this->addRoute('HEAD', $route, $method_path, $parameters);
273
    }
274
275
    /**
276
     * Add a new route with CLI as method.
277
     *
278
     * @param string $route The route
279
     * @param string $method_path The Method Path
280
     * @param array $parameters The parameters to pass
281
     */
282
    public function cli(string $route, string $method_path, array $parameters = []) : void
283
    {
284
        $this->addRoute('CLI', $route, $method_path, $parameters);
285
    }
286
287
    /**
288
     * Force the generation of the cache file. Delete the current cache file if it exists.
289
     */
290
    public function forceGenerateCache() : bool
291
    {
292
        $dispatchData = null;
0 ignored issues
show
Unused Code introduced by
$dispatchData is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
293
294
        if (file_exists($this->cacheFilePath)) {
295
            @unlink($this->cacheFilePath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
296
        }
297
298
        $dispatchData = $this->routeCollector->getData();
299
300
        return $this->generateCache($dispatchData);
301
    }
302
303
    /**
304
     * @return Dispatcher\GroupCountBased
305
     * @throws InvalidRouterCacheException if the router cache exists and is invalid.
306
     */
307
    private function getDispatcher() : Dispatcher\GroupCountBased
308
    {
309
        $dispatchData = null;
310
311
        if (_::getLibrary('Cervo/Config')->get('Production') == true && file_exists($this->cacheFilePath)) {
312
313
            $dispatchData = require $this->cacheFilePath;
314
315
            if (!is_array($dispatchData)) {
316
                throw new InvalidRouterCacheException;
317
            }
318
319
        } else {
320
321
            $dispatchData = $this->routeCollector->getData();
322
323
            if (_::getLibrary('Cervo/Config')->get('Production') == true) {
324
                $this->generateCache($dispatchData);
325
            }
326
327
        }
328
329
        return new Dispatcher\GroupCountBased($dispatchData);
330
    }
331
332
    /**
333
     * @param array $dispatchData
334
     *
335
     * @return bool
336
     */
337
    private function generateCache(array $dispatchData) : bool
338
    {
339
        $dir = dirname($this->cacheFilePath);
340
341
        if (!file_exists($this->cacheFilePath) && is_dir($dir) && is_writable($dir)) {
342
343
            return file_put_contents(
344
                    $this->cacheFilePath,
345
                    '<?php return ' . var_export($dispatchData, true) . ';' . PHP_EOL,
346
                    LOCK_EX
347
                ) !== false;
348
349
        } else {
350
            return false;
351
        }
352
    }
353
354
    /**
355
     * Returns a parsable URI
356
     *
357
     * @return string
358
     */
359
    private function detectUri() : string
360
    {
361
        if (php_sapi_name() == 'cli') {
362
            $args = array_slice($_SERVER['argv'], 1);
363
            return $args ? '/' . implode('/', $args) : '/';
364
        }
365
366
        if (!isset($_SERVER['REQUEST_URI']) || !isset($_SERVER['SCRIPT_NAME'])) {
367
            return '/';
368
        }
369
370
        $parts = preg_split('#\?#i', $this->getBaseUri(), 2);
371
        $uri = $parts[0];
372
373
        if ($uri == '/' || strlen($uri) <= 0) {
374
            return '/';
375
        }
376
377
        $uri = parse_url($uri, PHP_URL_PATH);
378
        return '/' . str_replace(['//', '../', '/..'], '/', trim($uri, '/'));
379
    }
380
381
    /**
382
     * Return the base URI for a request
383
     *
384
     * @return string
385
     */
386
    private function getBaseUri() : string
387
    {
388
        $uri = $_SERVER['REQUEST_URI'];
389
390
        if (strlen($_SERVER['SCRIPT_NAME']) > 0) {
391
392
            if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0) {
393
                $uri = substr($uri, strlen($_SERVER['SCRIPT_NAME']));
394
            } elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0) {
395
                $uri = substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME'])));
396
            }
397
398
        }
399
400
        return $uri;
401
    }
402
403
    /**
404
     * Throws an exception or return.
405
     *
406
     * @param array $middlewares
407
     * @param array $parameters
408
     * @param array $arguments
409
     *
410
     * @return void
411
     * @throws RouteMiddlewareFailedException if a route middleware returned false.
412
     * @throws InvalidMiddlewareException if a middleware is invalid.
413
     */
414
    private function handleMiddlewares(array $middlewares, array $parameters, array $arguments) : void
415
    {
416
        foreach ($middlewares as $middleware) {
417
418
            if (is_array($middleware) && strlen($middleware['library']) > 0 && strlen($middleware['method']) > 0) {
419
420
                $middleware_library = _::getLibrary($middleware['library']);
421
422
                if (!$middleware_library->{$middleware['method']}($parameters, $arguments)) {
423
                    throw new RouteMiddlewareFailedException;
424
                }
425
426
            } else {
427
                throw new InvalidMiddlewareException;
428
            }
429
430
        }
431
    }
432
}
433