Passed
Push — master ( 723c8d...5629e4 )
by Ch
02:31
created

AltoRouter.php (3 issues)

1
<?php
2
3
class AltoRouter
4
{
5
	protected $routes = array();
6
	protected $namedRoutes = array();
7
	protected $basePath = '';
8
	protected $matchTypes = array(
9
		'i'  => '[0-9]++',
10
		'a'  => '[0-9A-Za-z]++',
11
		'h'  => '[0-9A-Fa-f]++',
12
		'*'  => '.+?',
13
		'**' => '.++',
14
		''   => '[^/\.]++'
15
	);
16
	protected $all = array(
17
		'get', 'post'
18
	);
19
	private $server;
20
21
	/**
22
	 * Create router in one call from config.
23
	 *
24
	 * @param array $routes
25
	 * @param string $basePath
26
	 * @param array $matchTypes
27
	 */
28
	public function __construct($routes = array(), $basePath = '', $matchTypes = array(), $server = null)
0 ignored issues
show
__construct uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
29
	{
30
		$this->addRoutes($routes);
31
		$this->setBasePath($basePath);
32
		$this->addMatchTypes($matchTypes);
33
		if(!$server) {
34
			$this->server = $_SERVER;
35
		}
36
	}
37
38
	/**
39
	 * Retrieves all routes.
40
	 * Useful if you want to process or display routes.
41
	 * @return array All routes.
42
	 */
43
	public function getRoutes()
44
	{
45
		return $this->routes;
46
	}
47
48
	/**
49
	 * Add multiple routes at once from array in the following format:
50
	 *
51
	 *   $routes = array(
52
	 *      array($method, $route, $target, $name)
53
	 *   );
54
	 *
55
	 * @param array $routes
56
	 * @return void
57
	 * @author Koen Punt
58
	 */
59
	public function addRoutes($routes)
60
	{
61
		if (!is_array($routes) && !$routes instanceof Traversable) {
62
			throw new Exception('Routes should be an array or an instance of Traversable');
63
		}
64
		if(!empty($routes)) {
65
			foreach ($routes as $route) {
66
				call_user_func_array(array($this, 'map'), $route);
67
			}
68
		}
69
	}
70
71
	/**
72
	 * Set the base path.
73
	 * Useful if you are running your application from a subdirectory.
74
	 */
75
	public function setBasePath($basePath)
76
	{
77
		$this->basePath = $basePath;
78
	}
79
80
	/**
81
	 * Add named match types. It uses array_merge so keys can be overwritten.
82
	 *
83
	 * @param array $matchTypes The key is the name and the value is the regex.
84
	 */
85
	public function addMatchTypes($matchTypes)
86
	{
87
		$this->matchTypes = array_merge($this->matchTypes, $matchTypes);
88
	}
89
90
	/**
91
	 * Map a route to a target
92
	 *
93
	 * @param string $method One of 5 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|PATCH|PUT|DELETE)
94
	 * @param string $route The route regex, custom regex must start with an @. You can use multiple pre-set regex filters, like [i:id]
95
	 * @param mixed $target The target where this route should point to. Can be anything.
96
	 * @param string $name Optional name of this route. Supply if you want to reverse route this url in your application.
97
	 */
98
	public function map($method, $route, $target, $name = null)
99
	{
100
		if ($name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
101
			if (isset($this->namedRoutes[$name])) {
102
				throw new \Exception("Can not redeclare route '{$name}'");
103
			}
104
			$this->namedRoutes[$name] = $route;
105
		}
106
107
		$this->routes[] = array($method, $route, $target, $name);
108
	}
109
110
	/**
111
	 * Reversed routing
112
	 *
113
	 * Generate the URL for a named route. Replace regexes with supplied parameters
114
	 *
115
	 * @param string $routeName The name of the route.
116
	 * @param array @params Associative array of parameters to replace placeholders with.
117
	 * @return string The URL of the route with named parameters in place.
118
	 */
119
	public function generate($routeName, array $params = array())
120
	{
121
122
		// Check if named route exists
123
		if (!isset($this->namedRoutes[$routeName])) {
124
			throw new \Exception("Route '{$routeName}' does not exist.");
125
		}
126
127
		// Replace named parameters
128
		$route = $this->namedRoutes[$routeName];
129
130
		// prepend base path to route url again
131
		$url = $this->basePath . $route;
132
133
		if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) {
134
			foreach ($matches as $match) {
135
				$block  = $match[0];
136
				$pre    = $match[1];
137
				$param  = $match[3];
138
139
				if ($pre) {
140
					$block = substr($block, 1);
141
				}
142
143
				if (isset($params[$param])) {
144
					$url = str_replace($block, $params[$param], $url);
145
				} elseif ($match[4]) {
146
					$url = str_replace($pre . $block, '', $url);
147
				}
148
			}
149
		}
150
151
		return $url;
152
	}
153
154
	/**
155
	 * Match a given Request Url against stored routes
156
	 * @param string $requestUrl
157
	 * @param string $requestMethod
158
	 * @return array|boolean Array with route information on success, false on failure (no match).
159
	 */
160
	public function match($requestUrl = null, $requestMethod = null)
161
	{
162
		$params = array();
163
164
		$requestUrl = $this->getRequestUrl($requestUrl);
165
166
		// set Request Method if it isn't passed as a parameter
167
		if (is_null($requestMethod)) {
168
			$requestMethod = $this->server['REQUEST_METHOD'];
169
		}
170
171
		foreach ($this->routes as $handler) {
172
			// Method did not match, continue to next route.
173
			if (!$this->methodMatch($handler[0], $requestMethod, $handler[1], $requestUrl)) {
174
				continue;
175
			}
176
177
			return array(
178
				'target' => $handler[2],
179
				'params' => array_filter($params, function ($k) { return !is_numeric($k); }, ARRAY_FILTER_USE_KEY),
180
				'name'   => $handler[3]
181
			);
182
		}
183
184
		return false;
185
	}
186
187
	/**
188
	 * Compile the regex for a given route (EXPENSIVE)
189
	 */
190
	private function compileRoute($routeString, $requestUrl)
191
	{
192
		$route = $this->getRoute($routeString, $requestUrl);
193
194
		if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) {
195
			$matchTypes = $this->matchTypes;
196
			foreach ($matches as $match) {
197
				list($block, $pre, $type, $param, $optional) = $match;
198
199
				if (isset($matchTypes[$type])) {
200
					$type = $matchTypes[$type];
201
				}
202
				if ($pre === '.') {
203
					$pre = '\.';
204
				}
205
206
				//Older versions of PCRE require the 'P' in (?P<named>)
207
				$pattern = '(?:'
208
				           . ($pre !== '' ? $pre : null)
209
				           . '('
210
				           . ($param !== '' ? "?P<$param>" : null)
211
				           . $type
212
				           . '))'
213
				           . ($optional !== '' ? '?' : null);
214
215
				$route = str_replace($block, $pattern, $route);
216
			}
217
		}
218
219
		return "`^$route$`u";
220
	}
221
222
	/**
223
	 * @param $requestUrl
224
	 *
225
	 * @return mixed
226
	 */
227
	private function getRequestUrl($requestUrl)
228
	{
229
		// set Request Url if it isn't passed as parameter
230
		if (is_null($requestUrl)) {
231
			$requestUrl = parse_url($this->server['REQUEST_URI'], PHP_URL_PATH);
232
		}
233
234
		return str_replace($this->basePath, '', $requestUrl);
235
	}
236
237
	/**
238
	 * @param $method
239
	 * @param $requestMethod
240
	 * @param $routeString
241
	 * @param $requestUrl
242
	 *
243
	 * @return mixed
244
	 */
245
	private function methodMatch($method, $requestMethod, $routeString, $requestUrl)
246
	{
247
		$method = strtolower($method);
248
		$requestMethod = strtolower($requestMethod);
249
		$methods = explode('|', $method);
250
251
		if(in_array($requestMethod, $methods))
252
		{
253
			if($routeString == '*') return true;
254
255
			if(is_array($routeString) && !empty($routeString)) {
256
				if($routeString[0] == '@') {
257
					return preg_match('`' . substr($routeString, 1) . '`u', $requestUrl, $params);
0 ignored issues
show
$routeString of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

257
					return preg_match('`' . substr(/** @scrutinizer ignore-type */ $routeString, 1) . '`u', $requestUrl, $params);
Loading history...
258
				}
259
				$regex = $this->compileRoute($routeString, $requestUrl);
260
				return preg_match($regex, $requestUrl, $params);
261
			}
262
		}
263
264
		return false;
265
	}
266
267
	/**
268
	 * @param $routeString
269
	 * @param $requestUrl
270
	 *
271
	 * @return bool|string
272
	 */
273
	private function getRoute($routeString, $requestUrl)
274
	{
275
		$iPointer = $jPointer = 0;
276
		$nPointer = isset($routeString[0]) ? $routeString[0] : null;
277
		$regex = $route = false;
278
279
		// Find the longest non-regex substring and match it against the URI
280
		while (true) {
281
			if (!isset($routeString[$iPointer])) {
282
				break;
283
			}
284
			if ($regex === false) {
285
				if(!$this->getRouteRegexCheck($nPointer, $jPointer, $iPointer, $routeString, $requestUrl)) {
286
					continue;
287
				}
288
				$jPointer++;
289
			}
290
			$route .= $routeString[$iPointer++];
291
		}
292
293
		return $route;
294
	}
295
296
	private function getRouteRegexCheck($nPointer, $jPointer, $iPointer, $routeString, $requestUrl)
297
	{
298
		$cPointer = $nPointer;
299
		$regex = in_array($cPointer, array('[', '(', '.'));
300
		if (!$regex && isset($routeString[$iPointer+1])) {
301
			$nPointer = $routeString[$iPointer + 1];
302
			$regex = in_array($nPointer, array('?', '+', '*', '{'));
303
		}
304
		if (!$regex && $cPointer !== '/' && (!isset($requestUrl[$jPointer]) || $cPointer !== $requestUrl[$jPointer])) {
305
			return false;
306
		}
307
		return true;
308
	}
309
310
	public function __call($method, $arguments)
311
	{
312
		if(!in_array($method, array('get', 'post', 'delete', 'put', 'patch', 'update', 'all'))) {
313
			throw new Exception($method . ' not exist in the '. __CLASS__);
314
		}
315
316
		$methods = $method == 'all' ? implode('|', $this->all) : $method;
317
318
		$route = array_merge(array($methods), $arguments);
319
320
		call_user_func_array(array($this, 'map'), $route);
321
	}
322
323
	public function __toString()
324
	{
325
		return 'AltoRouter';
326
	}
327
}
328