Completed
Push — 4.0 ( a1413f...48d77f )
by Marc André
02:14
created

Router::group()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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