Completed
Push — 3.0 ( 86ab4f...229cb4 )
by Marc André
02:06
created

Router::getBaseUri()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 12
rs 9.4285
cc 3
eloc 7
nc 3
nop 0
1
<?php
2
3
4
/**
5
 *
6
 * Copyright (c) 2015 Marc André "Manhim" 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\InvalidRouterCacheException;
37
use Cervo\Libraries\Exceptions\RouteNotFoundException;
38
use FastRoute\RouteCollector;
39
use FastRoute\RouteParser as RouteParser;
40
use FastRoute\DataGenerator as DataGenerator;
41
use FastRoute\Dispatcher as Dispatcher;
42
43
44
/**
45
 * Route manager for Cervo.
46
 *
47
 * @author Marc André Audet <[email protected]>
48
 */
49
class Router
50
{
51
    /**
52
     * FastRoute, null if usingCache is set
53
     * @var RouteCollector
54
     */
55
    protected $routeCollector;
56
57
    /**
58
     * FastRoute cache file path.
59
     * @var string
60
     */
61
    protected $cacheFilePath;
62
63
    /**
64
     * This is set to true if we are using the cache.
65
     * @var bool
66
     */
67
    protected $usingCache = false;
68
69
    /**
70
     * Set to true if we need to generate the cache
71
     * @var bool
72
     */
73
    protected $generateCache = false;
74
75
    /**
76
     * Initialize the route configurations.
77
     */
78
    public function __construct()
79
    {
80
        $config = _::getLibrary('Cervo/Config');
81
82
        $this->cacheFilePath = $config->get('Cervo/Application/Directory') . \DS . 'router.cache.php';
83
84
        if ($config->get('Production') == true && file_exists($this->cacheFilePath)) {
85
            $this->usingCache = true;
86
        } else {
87
88
            // TODO: This can be simplified by checking the Production config at the moment where we want to generate the cache
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
89
            if ($config->get('Production') == true) {
90
                $this->generateCache = true;
91
            }
92
93
            $this->routeCollector = new RouteCollector(
94
                new RouteParser\Std(),
95
                new DataGenerator\GroupCountBased()
96
            );
97
98
            foreach (glob($config->get('Cervo/Application/Directory') . '*' . \DS . 'Router.php', \GLOB_NOSORT | \GLOB_NOESCAPE) as $file) {
99
                $function = require $file;
100
101
                if (is_callable($function)) {
102
                    $function($this);
103
                }
104
            }
105
106
        }
107
    }
108
109
    /**
110
     * Dispatch the request to the router.
111
     *
112
     * @return bool|Route
113
     */
114
    public function dispatch()
115
    {
116
        $dispatcher = $this->getDispatcher();
117
118
        if (defined('STDIN')) {
119
            $request_method = 'CLI';
120
        } else {
121
            $request_method = $_SERVER['REQUEST_METHOD'];
122
        }
123
124
        $routeInfo = $dispatcher->dispatch($request_method, $this->detectUri());
125
126
        if ($routeInfo[0] === Dispatcher::FOUND) {
127
128
            $handler = $routeInfo[1];
129
            $arguments = $routeInfo[2];
130
            $middleware = $handler['middleware'];
131
132
            if (is_array($middleware) && count($middleware) === 2) {
133
                $middleware_library = _::getLibrary($middleware[0]);
134
135
                if (!$middleware_library->$middleware[1]($this)) {
136
                    return false;
137
                }
138
            }
139
140
            return new Route($handler['method_path'], $handler['parameters'], $arguments);
141
142
        } else {
143
            throw new RouteNotFoundException;
144
        }
145
    }
146
147
    /**
148
     * Add a new route.
149
     *
150
     * @param string $httpMethod The HTTP method, example: GET, POST, PATCH, etc.
151
     * @param string $route The route
152
     * @param string $method_path The Method Path
153
     * @param array $middleware Call a middleware before executing the route. The format is ['MyModule/MyLibrary', 'MyMethod']
154
     * @param array $parameters The parameters to pass
155
     */
156
    public function addRoute($httpMethod, $route, $method_path, $middleware = [], $parameters = [])
157
    {
158
        if ($this->usingCache) {
159
            return;
160
        }
161
162
        $this->routeCollector->addRoute($httpMethod, $route, [
163
            'method_path' => $method_path,
164
            'middleware' => $middleware,
165
            'parameters' => $parameters
166
        ]);
167
    }
168
169
    protected function getDispatcher()
170
    {
171
        $dispatchData = null;
172
173
        if ($this->usingCache) {
174
175
            $dispatchData = require $this->cacheFilePath;
176
177
            if (!is_array($dispatchData)) {
178
                throw new InvalidRouterCacheException;
179
            }
180
181
        } else {
182
            $dispatchData = $this->routeCollector->getData();
183
        }
184
185
        $dir = dirname($this->cacheFilePath);
186
187
        if ($this->generateCache && !file_exists($this->cacheFilePath) && is_dir($dir) && is_writable($dir)) {
188
            file_put_contents(
189
                $this->cacheFilePath,
190
                '<?php return ' . var_export($dispatchData, true) . ';' . PHP_EOL,
191
                LOCK_EX
192
            );
193
        }
194
195
        return new Dispatcher\GroupCountBased($dispatchData);
196
    }
197
198
    /**
199
     * Returns a parsable URI
200
     *
201
     * @return string
202
     */
203
    protected function detectUri()
204
    {
205
        if (defined('STDIN')) {
206
            $args = array_slice($_SERVER['argv'], 1);
207
            return $args ? '/' . implode('/', $args) : '/';
208
        }
209
210
        if (!isset($_SERVER['REQUEST_URI']) || !isset($_SERVER['SCRIPT_NAME'])) {
211
            return '/';
212
        }
213
214
        $uri = $this->getQueryStringUri($this->getBaseUri());
215
216
        if ($uri == '/' || empty($uri)) {
217
            return '/';
218
        }
219
220
        $uri = parse_url($uri, PHP_URL_PATH);
221
        return '/' . str_replace(['//', '../', '/..'], '/', trim($uri, '/'));
222
    }
223
224
    /**
225
     * Return the base URI for a request
226
     *
227
     * @return string
228
     */
229
    protected function getBaseUri()
230
    {
231
        $uri = $_SERVER['REQUEST_URI'];
232
233
        if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0) {
234
            $uri = substr($uri, strlen($_SERVER['SCRIPT_NAME']));
235
        } elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0) {
236
            $uri = substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME'])));
237
        }
238
239
        return $uri;
240
    }
241
242
    /**
243
     * Return the uri with the query string parsed if the request is made using the query string method
244
     *
245
     * @param string $baseUri
246
     *
247
     * @return string
248
     */
249
    protected function getQueryStringUri($baseUri)
250
    {
251
        $from_query_string = false;
252
253
        if (strpos($baseUri, '?/') === 0) {
254
            $from_query_string = true;
255
            $baseUri = substr($baseUri, 2);
256
        }
257
258
        $parts = preg_split('#\?#i', $baseUri, 2);
259
        $baseUri = $parts[0];
260
261
        if ($from_query_string) {
262
            if (isset($parts[1])) {
263
                $_SERVER['QUERY_STRING'] = $parts[1];
264
                parse_str($_SERVER['QUERY_STRING'], $_GET);
265
            } else {
266
                $_SERVER['QUERY_STRING'] = '';
267
                $_GET = [];
268
            }
269
        }
270
271
        return $baseUri;
272
    }
273
}
274