Completed
Branch 4.0 (52c68b)
by Marc André
02:54
created

Router::detectUri()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 2 Features 1
Metric Value
c 10
b 2
f 1
dl 0
loc 21
rs 7.551
cc 7
eloc 12
nc 5
nop 0
1
<?php
2
3
4
/**
5
 *
6
 * Copyright (c) 2010-2016 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\Libraries\Exceptions\InvalidMiddlewareException;
37
use Cervo\Libraries\Exceptions\InvalidRouterCacheException;
38
use Cervo\Libraries\Exceptions\MethodNotAllowedException;
39
use Cervo\Libraries\Exceptions\RouteMiddlewareFailedException;
40
use Cervo\Libraries\Exceptions\RouteNotFoundException;
41
use FastRoute\RouteCollector;
42
use FastRoute\RouteParser as RouteParser;
43
use FastRoute\DataGenerator as DataGenerator;
44
use FastRoute\Dispatcher as Dispatcher;
45
46
47
/**
48
 * Route manager for Cervo.
49
 *
50
 * @author Marc André Audet <[email protected]>
51
 */
52
class Router
53
{
54
    /**
55
     * FastRoute, null if usingCache is set
56
     * @var RouteCollector
57
     */
58
    protected $routeCollector = null;
59
60
    /**
61
     * FastRoute cache file path.
62
     * @var string
63
     */
64
    protected $cacheFilePath;
65
66
    /**
67
     * List of middlewares called using the middleware() method.
68
     * @var array
69
     */
70
    protected $currentMiddlewares = [];
71
72
    /**
73
     * Initialize the route configurations.
74
     */
75
    public function __construct()
76
    {
77
        $config = _::getLibrary('Cervo/Config');
78
79
        $this->cacheFilePath = $config->get('Cervo/Application/Directory') . \DS . 'router.cache.php';
80
81
        $this->routeCollector = new RouteCollector(
82
            new RouteParser\Std(),
83
            new DataGenerator\GroupCountBased()
84
        );
85
86 View Code Duplication
        foreach (glob($config->get('Cervo/Application/Directory') . '*' . \DS . 'Router.php', \GLOB_NOSORT | \GLOB_NOESCAPE) as $file) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
87
88
            $function = require $file;
89
90
            if (is_callable($function)) {
91
                $function($this);
92
            }
93
94
        }
95
    }
96
97
    /**
98
     * Encapsulate all the routes that are added from $func(Router) with this middleware.
99
     *
100
     * @param array $middleware A middleware. The format is ['MyModule/MyLibrary', 'MyMethod'].
101
     * @param callable $func
102
     */
103
    public function middleware($middleware, $func)
104
    {
105
        if (is_array($middleware) && count($middleware) == 2) {
106
107
            array_push($this->currentMiddlewares, $middleware);
108
109
            $func($this);
110
111
            array_pop($this->currentMiddlewares);
112
113
        }
114
    }
115
116
    /**
117
     * Dispatch the request to the router.
118
     *
119
     * @return bool|Route
120
     */
121
    public function dispatch()
122
    {
123
        $dispatcher = $this->getDispatcher();
124
125
        if (defined('STDIN')) {
126
            $request_method = 'CLI';
127
        } else {
128
            $request_method = $_SERVER['REQUEST_METHOD'];
129
        }
130
131
        $routeInfo = $dispatcher->dispatch($request_method, $this->detectUri());
132
133
        if ($routeInfo[0] === Dispatcher::FOUND) {
134
135
            $handler = $routeInfo[1];
136
            $arguments = $routeInfo[2];
137
            $middlewares = $handler['middlewares'];
138
139
            if (is_array($middlewares)) {
140
                $this->handleMiddlewares($middlewares, $handler['parameters'], $arguments);
141
            }
142
143
            return new Route($handler['method_path'], $handler['parameters'], $arguments);
144
145
        } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {
146
            throw new MethodNotAllowedException;
147
        } else {
148
            throw new RouteNotFoundException;
149
        }
150
    }
151
152
    /**
153
     * Add a new route.
154
     *
155
     * @param string|string[] $http_method The HTTP method, example: GET, POST, PATCH, CLI, etc. Can be an array of values.
156
     * @param string $route The route
157
     * @param string $method_path The Method Path
158
     * @param array $parameters The parameters to pass
159
     */
160
    public function addRoute($http_method, $route, $method_path, $parameters = [])
161
    {
162
        if (_::getLibrary('Cervo/Config')->get('Production') == true && file_exists($this->cacheFilePath)) {
163
            return;
164
        }
165
166
        $this->routeCollector->addRoute($http_method, $route, [
167
            'method_path' => $method_path,
168
            'middlewares' => $this->currentMiddlewares,
169
            'parameters' => $parameters
170
        ]);
171
    }
172
173
    protected function getDispatcher()
174
    {
175
        $dispatchData = null;
176
177
        if (_::getLibrary('Cervo/Config')->get('Production') == true && file_exists($this->cacheFilePath)) {
178
179
            $dispatchData = require $this->cacheFilePath;
180
181
            if (!is_array($dispatchData)) {
182
                throw new InvalidRouterCacheException;
183
            }
184
185
        } else {
186
            $dispatchData = $this->routeCollector->getData();
187
        }
188
189
        $this->generateCache($dispatchData);
190
191
        return new Dispatcher\GroupCountBased($dispatchData);
192
    }
193
194
    protected function generateCache($dispatchData)
195
    {
196
        $dir = dirname($this->cacheFilePath);
197
198
        if (_::getLibrary('Cervo/Config')->get('Production') == true && !file_exists($this->cacheFilePath) && is_dir($dir) && is_writable($dir)) {
199
            file_put_contents(
200
                $this->cacheFilePath,
201
                '<?php return ' . var_export($dispatchData, true) . ';' . PHP_EOL,
202
                LOCK_EX
203
            );
204
        }
205
    }
206
207
    /**
208
     * Returns a parsable URI
209
     *
210
     * @return string
211
     */
212
    protected function detectUri()
213
    {
214
        if (php_sapi_name() == 'cli') {
215
            $args = array_slice($_SERVER['argv'], 1);
216
            return $args ? '/' . implode('/', $args) : '/';
217
        }
218
219
        if (!isset($_SERVER['REQUEST_URI']) || !isset($_SERVER['SCRIPT_NAME'])) {
220
            return '/';
221
        }
222
223
        $parts = preg_split('#\?#i', $this->getBaseUri(), 2);
224
        $uri = $parts[0];
225
226
        if ($uri == '/' || strlen($uri) <= 0) {
227
            return '/';
228
        }
229
230
        $uri = parse_url($uri, PHP_URL_PATH);
231
        return '/' . str_replace(['//', '../', '/..'], '/', trim($uri, '/'));
232
    }
233
234
    /**
235
     * Return the base URI for a request
236
     *
237
     * @return string
238
     */
239
    protected function getBaseUri()
240
    {
241
        $uri = $_SERVER['REQUEST_URI'];
242
243
        if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0) {
244
            $uri = substr($uri, strlen($_SERVER['SCRIPT_NAME']));
245
        } elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0) {
246
            $uri = substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME'])));
247
        }
248
249
        return $uri;
250
    }
251
252
    /**
253
     * Throws an exception or return.
254
     *
255
     * @param array $middlewares
256
     * @param array $parameters
257
     * @param array $arguments
258
     *
259
     * @return void
260
     */
261
    protected function handleMiddlewares($middlewares, $parameters, $arguments)
262
    {
263
        foreach ($middlewares as $middleware) {
264
265
            if (is_array($middleware) && count($middleware) == 2) {
266
267
                $middleware_library = _::getLibrary($middleware[0]);
268
269
                if (!$middleware_library->$middleware[1]($parameters, $arguments)) {
270
                    throw new RouteMiddlewareFailedException;
271
                }
272
273
            } else {
274
                throw new InvalidMiddlewareException;
275
            }
276
277
        }
278
    }
279
}
280