Completed
Push — 3.0 ( e21f45...439730 )
by Marc André
03:53 queued 02:11
created

Router::getDispatcher()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 28
rs 6.7272
cc 7
eloc 15
nc 5
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(
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...
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
        if (strpos($baseUri, '?/') === 0) {
252
            $baseUri = substr($baseUri, 2);
253
        }
254
255
        $parts = preg_split('#\?#i', $baseUri, 2);
256
        $baseUri = $parts[0];
257
258
        if (isset($parts[1])) {
259
            $_SERVER['QUERY_STRING'] = $parts[1];
260
            parse_str($_SERVER['QUERY_STRING'], $_GET);
261
        } else {
262
            $_SERVER['QUERY_STRING'] = '';
263
            $_GET = [];
264
        }
265
266
        return $baseUri;
267
    }
268
}
269