RouteCollection   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 402
Duplicated Lines 8.46 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 8
Bugs 0 Features 5
Metric Value
c 8
b 0
f 5
dl 34
loc 402
rs 8.3999
wmc 46
lcom 1
cbo 7

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 4
A getIterator() 0 4 1
A offsetExists() 0 4 1
A count() 0 4 1
A __call() 0 21 3
A add() 0 14 2
A resource() 0 10 2
A offsetGet() 0 14 3
A offsetSet() 0 5 1
A offsetUnset() 0 6 1
D find() 34 107 18
A revoke_cache() 0 5 1
B sort_routes() 0 38 5
A filter() 0 16 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like RouteCollection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RouteCollection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ICanBoogie\Routing;
13
14
use ICanBoogie\HTTP\Request;
15
use ICanBoogie\Prototype\MethodNotDefined;
16
17
/**
18
 * A route collection.
19
 *
20
 * @method RouteCollection any() any(string $pattern, $controller, array $options=[]) Add a route for any HTTP method.
21
 * @method RouteCollection connect() connect(string $pattern, $controller, array $options=[]) Add a route for the HTTP method CONNECT.
22
 * @method RouteCollection delete() delete(string $pattern, $controller, array $options=[]) Add a route for the HTTP method DELETE.
23
 * @method RouteCollection get() get(string $pattern, $controller, array $options=[]) Add a route for the HTTP method GET.
24
 * @method RouteCollection head() head(string $pattern, $controller, array $options=[]) Add a route for the HTTP method HEAD.
25
 * @method RouteCollection options() options(string $pattern, $controller, array $options=[]) Add a route for the HTTP method OPTIONS.
26
 * @method RouteCollection post() post(string $pattern, $controller, array $options=[]) Add a route for the HTTP method POST.
27
 * @method RouteCollection put() put(string $pattern, $controller, array $options=[]) Add a route for the HTTP method PUT.
28
 * @method RouteCollection patch() patch(string $pattern, $controller, array $options=[]) Add a route for the HTTP method PATCH
29
 * @method RouteCollection trace() trace(string $pattern, $controller, array $options=[]) Add a route for the HTTP method TRACE.
30
 */
31
class RouteCollection implements \IteratorAggregate, \ArrayAccess, \Countable
32
{
33
	/**
34
	 * Specify that the route definitions can be trusted.
35
	 */
36
	const TRUSTED_DEFINITIONS = true;
37
38
	/**
39
	 * Class name of the {@link Route} instances.
40
	 */
41
	const DEFAULT_ROUTE_CLASS = Route::class;
42
43
	/**
44
	 * Route definitions.
45
	 *
46
	 * @var array
47
	 */
48
	protected $routes = [];
49
50
	/**
51
	 * Route instances.
52
	 *
53
	 * @var Route[]
54
	 */
55
	protected $instances = [];
56
57
	/**
58
	 * @param array $definitions
59
	 * @param bool $trusted_definitions {@link TRUSTED_DEFINITIONS} if the definition can be
60
	 * trusted. This will speed up the construct process but the definitions will not be checked,
61
	 * nor will they be normalized.
62
	 */
63
	public function __construct(array $definitions = [], $trusted_definitions = false)
64
	{
65
		foreach ($definitions as $id => $definition)
66
		{
67
			if (is_string($id) && empty($definition[RouteDefinition::ID]))
68
			{
69
				$definition[RouteDefinition::ID] = $id;
70
			}
71
72
			$this->add($definition, $trusted_definitions);
73
		}
74
	}
75
76
	/**
77
	 * Adds a route definition using an HTTP method.
78
	 *
79
	 * @param string $method
80
	 * @param array $arguments
81
	 *
82
	 * @return $this
83
	 */
84
	public function __call($method, array $arguments)
85
	{
86
		$method = strtoupper($method);
87
88
		if ($method !== Request::METHOD_ANY && !in_array($method, Request::$methods))
89
		{
90
			throw new MethodNotDefined($method, $this);
91
		}
92
93
		list($pattern, $controller, $options) = $arguments + [ 2 => [] ];
94
95
		$this->revoke_cache();
96
		$this->add([
97
98
			RouteDefinition::CONTROLLER => $controller,
99
			RouteDefinition::PATTERN => $pattern
100
101
		] + $options + [ RouteDefinition::VIA => $method ]);
102
103
		return $this;
104
	}
105
106
	/**
107
	 * Adds a route definition.
108
	 *
109
	 * **Note:** The method does *not* revoke cache.
110
	 *
111
	 * @param array $definition
112
	 * @param bool $trusted_definition {@link TRUSTED_DEFINITIONS} if the method should be trusting the
113
	 * definition, in which case the method doesn't assert if the definition is valid, nor does
114
	 * it normalizes it.
115
	 *
116
	 * @return $this
117
	 */
118
	protected function add(array $definition, $trusted_definition = false)
119
	{
120
		if (!$trusted_definition)
121
		{
122
			RouteDefinition::assert_is_valid($definition);
123
			RouteDefinition::normalize($definition);
124
			RouteDefinition::ensure_has_id($definition);
125
		}
126
127
		$id = $definition[RouteDefinition::ID];
128
		$this->routes[$id] = $definition;
129
130
		return $this;
131
	}
132
133
	/**
134
	 * Adds resource routes.
135
	 *
136
	 * **Note:** The route definitions for the resource are created by
137
	 * {@link RouteMaker::resource}. Both methods accept the same arguments.
138
	 *
139
	 * @see \ICanBoogie\Routing\RoutesMaker::resource
140
	 *
141
	 * @param string $name
142
	 * @param string $controller
143
	 * @param array $options
144
	 *
145
	 * @return array
146
	 */
147
	public function resource($name, $controller, array $options = [])
148
	{
149
		$definitions = RouteMaker::resource($name, $controller, $options);
150
		$this->revoke_cache();
151
152
		foreach ($definitions as $id => $definition)
153
		{
154
			$this->add([ RouteDefinition::ID => $id ] + $definition);
155
		}
156
	}
157
158
	public function getIterator()
159
	{
160
		return new \ArrayIterator($this->routes);
161
	}
162
163
	public function offsetExists($offset)
164
	{
165
		return isset($this->routes[$offset]);
166
	}
167
168
	/**
169
	 * Returns a {@link Route} instance.
170
	 *
171
	 * @param string $id Route identifier.
172
	 *
173
	 * @return Route
174
	 *
175
	 * @throws RouteNotDefined
176
	 */
177
	public function offsetGet($id)
178
	{
179
		if (isset($this->instances[$id]))
180
		{
181
			return $this->instances[$id];
182
		}
183
184
		if (!$this->offsetExists($id))
185
		{
186
			throw new RouteNotDefined($id);
187
		}
188
189
		return $this->instances[$id] = Route::from($this->routes[$id]);
190
	}
191
192
	/**
193
	 * Defines a route.
194
	 *
195
	 * @param string $id The identifier of the route.
196
	 * @param array $route The route definition.
197
	 */
198
	public function offsetSet($id, $route)
199
	{
200
		$this->revoke_cache();
201
		$this->add([ RouteDefinition::ID => $id ] + $route);
202
	}
203
204
	/**
205
	 * Removes a route.
206
	 *
207
	 * @param string $offset The identifier of the route.
208
	 */
209
	public function offsetUnset($offset)
210
	{
211
		unset($this->routes[$offset]);
212
213
		$this->revoke_cache();
214
	}
215
216
	/**
217
	 * Returns the number of routes in the collection.
218
	 *
219
	 * @return int
220
	 */
221
	public function count()
222
	{
223
		return count($this->routes);
224
	}
225
226
	/**
227
	 * Search for a route matching the specified pathname and method.
228
	 *
229
	 * @param string $uri The URI to match. If the URI includes a query string it is removed
230
	 * before searching for a matching route.
231
	 * @param array|null $captured The parameters captured from the URI. If the URI included a
232
	 * query string, its parsed params are stored under the `__query__` key.
233
	 * @param string $method One of HTTP\Request::METHOD_* methods.
234
	 *
235
	 * @return Route|false|null
236
	 */
237
	public function find($uri, &$captured = null, $method = Request::METHOD_ANY)
238
	{
239
		$captured = [];
240
241
		$parsed = (array) parse_url($uri) + [ 'path' => null, 'query' => null ];
242
		$path = $parsed['path'];
243
244
		if (!$path)
245
		{
246
			return false;
247
		}
248
249
		#
250
		# Determine if a route matches prerequisites.
251
		#
252
		$matchable = function($via) use($method) {
253
254
			if ($method != Request::METHOD_ANY)
255
			{
256
				if (is_array($via))
257
				{
258
					if (!in_array($method, $via))
259
					{
260
						return false;
261
					}
262
				}
263
				else if ($via !== Request::METHOD_ANY && $via !== $method)
264
				{
265
					return false;
266
				}
267
			}
268
269
			return true;
270
		};
271
272
		#
273
		# Search for a matching static route.
274
		#
275 View Code Duplication
		$map_static = function($definitions) use($path, &$matchable) {
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...
276
277
			foreach ($definitions as $id => $definition)
278
			{
279
				$pattern = $definition[RouteDefinition::PATTERN];
280
				$via = $definition[RouteDefinition::VIA];
281
282
				if (!$matchable($via) || $pattern != $path)
283
				{
284
					continue;
285
				}
286
287
				return $id;
288
			}
289
290
			return null;
291
		};
292
293
		#
294
		# Search for a matching dynamic route.
295
		#
296 View Code Duplication
		$map_dynamic = function($definitions) use($path, &$matchable, &$captured) {
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...
297
298
			foreach ($definitions as $id => $definition)
299
			{
300
				$pattern = $definition[RouteDefinition::PATTERN];
301
				$via = $definition[RouteDefinition::VIA];
302
303
				if (!$matchable($via) || !Pattern::from($pattern)->match($path, $captured))
304
				{
305
					continue;
306
				}
307
308
				return $id;
309
			}
310
311
			return null;
312
		};
313
314
		list($static, $dynamic) = $this->sort_routes();
315
316
		$id = null;
317
318
		if ($static)
319
		{
320
			$id = $map_static($static);
321
		}
322
323
		if (!$id && $dynamic)
324
		{
325
			$id = $map_dynamic($dynamic);
326
		}
327
328
		if (!$id)
329
		{
330
			return null;
331
		}
332
333
		$query = $parsed['query'];
334
335
		if ($query)
336
		{
337
			parse_str($query, $parsed_query_string);
338
339
			$captured['__query__'] = $parsed_query_string;
340
		}
341
342
		return $this[$id];
343
	}
344
345
	private $static;
346
	private $dynamic;
347
348
	/**
349
	 * Revokes the cache used by the {@link sort_routes} method.
350
	 */
351
	private function revoke_cache()
352
	{
353
		$this->static = null;
354
		$this->dynamic = null;
355
	}
356
357
	/**
358
	 * Sorts routes according to their type and computed weight.
359
	 *
360
	 * Routes and grouped in two groups: static routes and dynamic routes. The difference between
361
	 * static and dynamic routes is that dynamic routes capture parameters from the path and thus
362
	 * require a regex to compute the match, whereas static routes only require is simple string
363
	 * comparison.
364
	 *
365
	 * Dynamic routes are ordered according to their weight, which is computed from the number
366
	 * of static parts before the first capture. The more static parts, the lighter the route is.
367
	 *
368
	 * @return array An array with the static routes and dynamic routes.
369
	 */
370
	private function sort_routes()
371
	{
372
		if ($this->static !== null)
373
		{
374
			return [ $this->static, $this->dynamic ];
375
		}
376
377
		$static = [];
378
		$dynamic = [];
379
		$weights = [];
380
381
		foreach ($this->routes as $id => $definition)
382
		{
383
			$pattern = $definition[RouteDefinition::PATTERN];
384
			$first_capture_position = strpos($pattern, ':') ?: strpos($pattern, '<');
385
386
			if ($first_capture_position === false)
387
			{
388
				$static[$id] = $definition;
389
			}
390
			else
391
			{
392
				$dynamic[$id] = $definition;
393
				$weights[$id] = substr_count($pattern, '/', 0, $first_capture_position);
394
			}
395
		}
396
397
		\ICanBoogie\stable_sort($dynamic, function($v, $k) use($weights) {
398
399
			return -$weights[$k];
400
401
		});
402
403
		$this->static = $static;
404
		$this->dynamic = $dynamic;
405
406
		return [ $static, $dynamic ];
407
	}
408
409
	/**
410
	 * Returns a new collection with filtered routes.
411
	 *
412
	 * @param callable $filter
413
	 *
414
	 * @return RouteCollection
415
	 */
416
	public function filter(callable $filter)
417
	{
418
		$definitions = [];
419
420
		foreach ($this as $id => $definition)
421
		{
422
			if (!$filter($definition, $id))
423
			{
424
				continue;
425
			}
426
427
			$definitions[$id] = $definition;
428
		}
429
430
		return new static($definitions);
431
	}
432
}
433