Completed
Push — 4.0 ( 23721f...314069 )
by Marc André
04:48 queued 02:38
created

Router::delete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 3
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
class Router
54
{
55
    /**
56
     * FastRoute, null if usingCache is set
57
     * @var RouteCollector
58
     */
59
    protected $routeCollector = null;
60
61
    /**
62
     * FastRoute cache file path.
63
     * @var string
64
     */
65
    protected $cacheFilePath;
66
67
    /**
68
     * List of middlewares called using the middleware() method.
69
     * @var array
70
     */
71
    protected $currentMiddlewares = [];
72
73
    /**
74
     * List of group prefixes called using the group() method.
75
     * @var string
76
     */
77
    protected $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 View Code Duplication
        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($library_name, $method_name, callable $func)
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($prefix, callable $func)
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 bool|Route
146
     */
147
    public function dispatch()
148
    {
149
        $dispatcher = $this->getDispatcher();
150
151
        if (defined('STDIN')) {
152
            $request_method = 'CLI';
153
        } else {
154
            $request_method = $_SERVER['REQUEST_METHOD'];
155
        }
156
157
        $routeInfo = $dispatcher->dispatch($request_method, $this->detectUri());
158
159
        if ($routeInfo[0] === Dispatcher::FOUND) {
160
161
            $handler = $routeInfo[1];
162
            $arguments = $routeInfo[2];
163
            $middlewares = $handler['middlewares'];
164
165
            if (is_array($middlewares)) {
166
                $this->handleMiddlewares($middlewares, $handler['parameters'], $arguments);
167
            }
168
169
            return new Route($handler['method_path'], $handler['parameters'], $arguments);
170
171
        } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {
172
            throw new MethodNotAllowedException;
173
        } else {
174
            throw new RouteNotFoundException;
175
        }
176
    }
177
178
    /**
179
     * Add a new route.
180
     *
181
     * @param string|string[] $http_method The HTTP method, example: GET, POST, PATCH, PUT, DELETE, CLI, etc. Can be an array of values.
182
     * @param string $route The route
183
     * @param string $method_path The Method Path
184
     * @param array $parameters The parameters to pass
185
     */
186
    public function addRoute($http_method, $route, $method_path, $parameters = [])
187
    {
188
        if (_::getLibrary('Cervo/Config')->get('Production') == true && file_exists($this->cacheFilePath)) {
189
            return;
190
        }
191
192
        $route = $this->currentGroupPrefix . $route;
193
194
        $this->routeCollector->addRoute($http_method, $route, [
195
            'method_path' => $method_path,
196
            'middlewares' => $this->currentMiddlewares,
197
            'parameters' => $parameters
198
        ]);
199
    }
200
201
    /**
202
     * Add a new route with GET as HTTP method.
203
     *
204
     * @param string $route The route
205
     * @param string $method_path The Method Path
206
     * @param array $parameters The parameters to pass
207
     */
208
    public function get($route, $method_path, $parameters = [])
209
    {
210
        $this->addRoute('GET', $route, $method_path, $parameters);
211
    }
212
213
    /**
214
     * Add a new route with POST as HTTP method.
215
     *
216
     * @param string $route The route
217
     * @param string $method_path The Method Path
218
     * @param array $parameters The parameters to pass
219
     */
220
    public function post($route, $method_path, $parameters = [])
221
    {
222
        $this->addRoute('POST', $route, $method_path, $parameters);
223
    }
224
225
    /**
226
     * Add a new route with PUT as HTTP method.
227
     *
228
     * @param string $route The route
229
     * @param string $method_path The Method Path
230
     * @param array $parameters The parameters to pass
231
     */
232
    public function put($route, $method_path, $parameters = [])
233
    {
234
        $this->addRoute('PUT', $route, $method_path, $parameters);
235
    }
236
237
    /**
238
     * Add a new route with PATCH as HTTP method.
239
     *
240
     * @param string $route The route
241
     * @param string $method_path The Method Path
242
     * @param array $parameters The parameters to pass
243
     */
244
    public function patch($route, $method_path, $parameters = [])
245
    {
246
        $this->addRoute('PATCH', $route, $method_path, $parameters);
247
    }
248
249
    /**
250
     * Add a new route with DELETE as HTTP method.
251
     *
252
     * @param string $route The route
253
     * @param string $method_path The Method Path
254
     * @param array $parameters The parameters to pass
255
     */
256
    public function delete($route, $method_path, $parameters = [])
257
    {
258
        $this->addRoute('DELETE', $route, $method_path, $parameters);
259
    }
260
261
    /**
262
     * Add a new route with HEAD as HTTP method.
263
     *
264
     * @param string $route The route
265
     * @param string $method_path The Method Path
266
     * @param array $parameters The parameters to pass
267
     */
268
    public function head($route, $method_path, $parameters = [])
269
    {
270
        $this->addRoute('HEAD', $route, $method_path, $parameters);
271
    }
272
273
    /**
274
     * Add a new route with CLI as method.
275
     *
276
     * @param string $route The route
277
     * @param string $method_path The Method Path
278
     * @param array $parameters The parameters to pass
279
     */
280
    public function cli($route, $method_path, $parameters = [])
281
    {
282
        $this->addRoute('CLI', $route, $method_path, $parameters);
283
    }
284
285
    protected function getDispatcher()
286
    {
287
        $dispatchData = null;
288
289
        if (_::getLibrary('Cervo/Config')->get('Production') == true && file_exists($this->cacheFilePath)) {
290
291
            $dispatchData = require $this->cacheFilePath;
292
293
            if (!is_array($dispatchData)) {
294
                throw new InvalidRouterCacheException;
295
            }
296
297
        } else {
298
            $dispatchData = $this->routeCollector->getData();
299
        }
300
301
        $this->generateCache($dispatchData);
302
303
        return new Dispatcher\GroupCountBased($dispatchData);
304
    }
305
306
    protected function generateCache($dispatchData)
307
    {
308
        $dir = dirname($this->cacheFilePath);
309
310
        if (_::getLibrary('Cervo/Config')->get('Production') == true && !file_exists($this->cacheFilePath) && is_dir($dir) && is_writable($dir)) {
311
            file_put_contents(
312
                $this->cacheFilePath,
313
                '<?php return ' . var_export($dispatchData, true) . ';' . PHP_EOL,
314
                LOCK_EX
315
            );
316
        }
317
    }
318
319
    /**
320
     * Returns a parsable URI
321
     *
322
     * @return string
323
     */
324
    protected function detectUri()
325
    {
326
        if (php_sapi_name() == 'cli') {
327
            $args = array_slice($_SERVER['argv'], 1);
328
            return $args ? '/' . implode('/', $args) : '/';
329
        }
330
331
        if (!isset($_SERVER['REQUEST_URI']) || !isset($_SERVER['SCRIPT_NAME'])) {
332
            return '/';
333
        }
334
335
        $parts = preg_split('#\?#i', $this->getBaseUri(), 2);
336
        $uri = $parts[0];
337
338
        if ($uri == '/' || strlen($uri) <= 0) {
339
            return '/';
340
        }
341
342
        $uri = parse_url($uri, PHP_URL_PATH);
343
        return '/' . str_replace(['//', '../', '/..'], '/', trim($uri, '/'));
344
    }
345
346
    /**
347
     * Return the base URI for a request
348
     *
349
     * @return string
350
     */
351
    protected function getBaseUri()
352
    {
353
        $uri = $_SERVER['REQUEST_URI'];
354
355
        if (strlen($_SERVER['SCRIPT_NAME']) > 0) {
356
357
            if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0) {
358
                $uri = substr($uri, strlen($_SERVER['SCRIPT_NAME']));
359
            } elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0) {
360
                $uri = substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME'])));
361
            }
362
363
        }
364
365
        return $uri;
366
    }
367
368
    /**
369
     * Throws an exception or return.
370
     *
371
     * @param array $middlewares
372
     * @param array $parameters
373
     * @param array $arguments
374
     *
375
     * @return void
376
     */
377
    protected function handleMiddlewares($middlewares, $parameters, $arguments)
378
    {
379
        foreach ($middlewares as $middleware) {
380
381
            if (is_array($middleware) && strlen($middleware['library']) > 0 && strlen($middleware['method']) > 0) {
382
383
                $middleware_library = _::getLibrary($middleware['library']);
384
385
                if (!$middleware_library->{$middleware['method']}($parameters, $arguments)) {
386
                    throw new RouteMiddlewareFailedException;
387
                }
388
389
            } else {
390
                throw new InvalidMiddlewareException;
391
            }
392
393
        }
394
    }
395
}
396