Completed
Push — master ( d240eb...82cfa8 )
by Pavel
02:18
created

ApiRoute   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 434
Duplicated Lines 5.07 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 6
dl 22
loc 434
rs 6.8
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A setPresenter() 0 4 1
A getPresenter() 0 4 1
A setAction() 0 11 3
A prepareForMatch() 0 4 1
A getPlacehodlerParameters() 0 14 2
A getRequiredParams() 0 17 2
A resolveFormat() 0 18 4
A getFormatFull() 0 4 1
B __construct() 11 38 6
A setMethods() 11 14 4
A getMethods() 0 4 1
A resolveMethod() 0 13 4
D match() 0 107 16
C constructUrl() 0 46 9

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 ApiRoute 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 ApiRoute, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @copyright   Copyright (c) 2016 ublaboo <[email protected]>
5
 * @author      Pavel Janda <[email protected]>
6
 * @package     Ublaboo
7
 */
8
9
namespace Ublaboo\ApiRouter;
10
11
use Nette\Application\IRouter;
12
use Nette\Application\Request;
13
use Nette;
14
use Nette\Utils\Strings;
15
16
/**
17
 * @method mixed onMatch(static, Nette\Application\Request $request)
18
 * 
19
 * @Annotation
20
 * @Target({"CLASS", "METHOD"})
21
 */
22
class ApiRoute extends ApiRouteSpec implements IRouter
23
{
24
25
	/**
26
	 * @var callable[]
27
	 */
28
	public $onMatch;
29
30
	/**
31
	 * @var string
32
	 */
33
	private $presenter;
34
35
	/**
36
	 * @var array
37
	 */
38
	private $actions = [
39
		'POST'    => FALSE,
40
		'GET'     => FALSE,
41
		'PUT'     => FALSE,
42
		'DELETE'  => FALSE,
43
		'OPTIONS' => FALSE
44
	];
45
46
	/**
47
	 * @var array
48
	 */
49
	private $default_actions = [
50
		'POST'    => 'create',
51
		'GET'     => 'read',
52
		'PUT'     => 'update',
53
		'DELETE'  => 'delete',
54
		'OPTIONS' => 'options'
55
	];
56
57
	/**
58
	 * @var array
59
	 */
60
	private $formats = [
61
		'json' => 'application/json',
62
		'xml'  => 'application/xml'
63
	];
64
65
	/**
66
	 * @var array
67
	 */
68
	private $placeholder_order = [];
69
70
71
	/**
72
	 * @param mixed  $data
73
	 * @param string $presenter
74
	 * @param array  $data
75
	 */
76
	public function __construct($path, $presenter = NULL, array $data = [])
77
	{
78
		/**
79
		 * Interface for setting route via annotation or directly
80
		 */
81
		if (!is_array($path)) {
82
			$data['value'] = $path;
83
			$data['presenter'] = $presenter;
84
85
			if (empty($data['methods'])) {
86
				$this->actions = $this->default_actions;
87
			} else {
88 View Code Duplication
				foreach ($data['methods'] as $method => $action) {
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...
89
					if (is_string($method)) {
90
						$this->setAction($action, $method);
91
					} else {
92
						$m = $action;
93
94
						if (isset($this->default_actions[$m])) {
95
							$this->setAction($this->default_actions[$m], $m);
96
						}
97
					}
98
				}
99
100
				unset($data['methods']);
101
			}
102
		} else {
103
			$data = $path;
104
		}
105
106
		/**
107
		 * Set Path
108
		 */
109
		$this->setPath($data['value']);
110
		unset($data['value']);
111
112
		parent::__construct($data);
113
	}
114
115
116
	/**
117
	 * @param string $presenter
118
	 * @return void
119
	 */
120
	public function setPresenter($presenter)
121
	{
122
		$this->presenter = $presenter;
123
	}
124
125
126
	/**
127
	 * @return string
128
	 */
129
	public function getPresenter()
130
	{
131
		return $this->presenter;
132
	}
133
134
135
	/**
136
	 * @param string $action
137
	 * @param string $method
138
	 * @return void
139
	 */
140
	public function setAction($action, $method = NULL) {
141
		if (is_null($method)) {
142
			$method = array_search($action, $this->default_actions);
143
		}
144
145
		if (!isset($this->default_actions[$method])) {
146
			return;
147
		}
148
149
		$this->actions[$method] = $action;
150
	}
151
152
153
	/**
154
	 * @param  string $string
155
	 * @return string
156
	 */
157
	private function prepareForMatch($string)
158
	{
159
		return sprintf('/%s/', str_replace('/', '\/', $string));
160
	}
161
162
163
	/**
164
	 * Get all parameters from url mask
165
	 * @return array
166
	 */
167
	public function getPlacehodlerParameters()
168
	{
169
		if (!empty($this->placeholder_order)) {
170
			return array_filter($this->placeholder_order);
171
		}
172
173
		$return = [];
174
175
		preg_replace_callback('/<(\w+)>/', function($item) use (&$return) {
176
			$return[] = end($item);
177
		}, $this->path);
178
179
		return $return;
180
	}
181
182
183
	/**
184
	 * Get required parameters from url mask
185
	 * @return array
186
	 */
187
	public function getRequiredParams()
188
	{
189
		$regex = '/\[[^\[]+?\]/';
190
		$path = $this->getPath();
191
192
		while (preg_match($regex, $path)) {
193
			$path = preg_replace($regex, '', $path);
194
		}
195
196
		$required = [];
197
198
		preg_replace_callback('/<(\w+)>/', function($item) use (&$required) {
199
			$required[] = end($item);
200
		}, $path);
201
202
		return $required;
203
	}
204
205
206
	/**
207
	 * @param  Nette\Http\IRequest $httpRequest
208
	 * @return void
209
	 */
210
	public function resolveFormat(Nette\Http\IRequest $httpRequest)
211
	{
212
		if ($this->getFormat()) {
213
			return;
214
		}
215
216
		$header = $httpRequest->getHeader('Accept');
217
218
		foreach ($this->formats as $format => $format_full) {
219
			$format_full = Strings::replace($format_full, '/\//', '\/');
220
221
			if (Strings::match($header, "/{$format_full}/")) {
222
				$this->setFormat($format);
223
			}
224
		}
225
226
		$this->setFormat('json');
227
	}
228
229
230
	/**
231
	 * @return string
232
	 */
233
	public function getFormatFull()
234
	{
235
		return $this->formats[$this->getFormat()];
236
	}
237
238
239
	/**
240
	 * @param array $methods
241
	 */
242
	public function setMethods(array $methods)
243
	{
244 View Code Duplication
		foreach ($methods as $method => $action) {
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...
245
			if (is_string($method)) {
246
				$this->setAction($action, $method);
247
			} else {
248
				$m = $action;
249
250
				if (isset($this->default_actions[$m])) {
251
					$this->setAction($this->default_actions[$m], $m);
252
				}
253
			}
254
		}
255
	}
256
257
258
	/**
259
	 * @return array
260
	 */
261
	public function getMethods()
262
	{
263
		return array_keys(array_filter($this->actions));
264
	}
265
266
267
	/**
268
	 * @param  Nette\Http\IRequest $request
269
	 * @return string
270
	 */
271
	public function resolveMethod(Nette\Http\IRequest $request) {
272
		if (!empty($request->getHeader('X-HTTP-Method-Override'))) {
273
			return Strings::upper($request->getHeader('X-HTTP-Method-Override'));
274
		}
275
276
		if ($method = Strings::upper($request->getQuery('__apiRouteMethod'))) {
277
			if (isset($this->actions[$method])) {
278
				return $method;
279
			}
280
		}
281
282
		return Strings::upper($request->getMethod());
283
	}
284
285
286
	/********************************************************************************
287
	 *                              Interface IRouter                               *
288
	 ********************************************************************************/
289
290
291
	/**
292
	 * Maps HTTP request to a Request object.
293
	 * @return Request|NULL
294
	 */
295
	public function match(Nette\Http\IRequest $httpRequest)
296
	{
297
		/**
298
		 * ApiRoute can be easily disabled
299
		 */
300
		if ($this->disable) {
301
			return NULL;
302
		}
303
304
		$url = $httpRequest->getUrl();
305
306
		$path = '/' . preg_replace('/^' . str_replace('/', '\/', preg_quote($url->getBasePath())) . '/', '', $url->getPath());
307
308
		/**
309
		 * Build path mask
310
		 */
311
		$order = &$this->placeholder_order;
312
		$parameters = $this->parameters;
313
314
		$mask = preg_replace_callback('/(<(\w+)>)|\[|\]/', function($item) use (&$order, $parameters) {
315
			if ($item[0] == '[' || $item[0] == ']') {
316
				if ($item[0] == '[') {
317
					$order[] = NULL;
318
				}
319
320
				return $item[0];
321
			}
322
323
			list(,, $placeholder) = $item;
324
325
			$parameter = isset($parameters[$placeholder]) ? $parameters[$placeholder] : [];
326
327
			$regex = isset($parameter['requirement']) ? $parameter['requirement'] : '\w+';
328
			$has_default = array_key_exists('default', $parameter);
329
330
			if ($has_default) {
331
				$order[] = $placeholder;
332
333
				return sprintf('(%s)?', $regex);
334
			}
335
336
			$order[] = $placeholder;
337
338
			return sprintf('(%s)', $regex);
339
		}, $this->path);
340
341
		$mask = '^' . str_replace(['[', ']'], ['(', ')?'], $mask) . '$';
342
343
		/**
344
		 * Prepare paths for regex match (escape slashes)
345
		 */
346
		if (!preg_match_all($this->prepareForMatch($mask), $path, $matches)) {
347
			return NULL;
348
		}
349
350
		/**
351
		 * Did some action to the request method exists?
352
		 */
353
		$this->resolveFormat($httpRequest);
354
		$method = $this->resolveMethod($httpRequest);
355
		$action = $this->actions[$method];
356
357
		if (!$action) {
358
			return NULL;
359
		}
360
361
		/**
362
		 * Basic params
363
		 */
364
		$params = $httpRequest->getQuery();
365
		$params['action'] = $action;
366
		$required_params = $this->getRequiredParams();
367
368
		/**
369
		 * Route mask parameters
370
		 */
371
		array_shift($matches);
372
373
		foreach ($this->placeholder_order as $key => $name) {
374
			if (NULL !== $name && isset($matches[$key])) {
375
				$params[$name] = reset($matches[$key]) ?: NULL;
376
377
				/**
378
				 * Required parameters
379
				 */
380
				if (empty($params[$name]) && in_array($name, $required_params)) {
381
					return NULL;
382
				}
383
			}
384
		}
385
386
		$request = new Request(
387
			$this->presenter,
388
			$method,
389
			$params,
390
			$httpRequest->getPost(),
391
			$httpRequest->getFiles(),
392
			[Request::SECURED => $httpRequest->isSecured()]
393
		);
394
395
		/**
396
		 * Trigger event - route matches
397
		 */
398
		$this->onMatch($this, $request);
399
400
		return $request;
401
	}
402
403
404
	/**
405
	 * Constructs absolute URL from Request object.
406
	 * @return string|NULL
407
	 */
408
	public function constructUrl(Request $request, Nette\Http\Url $url)
409
	{
410
		if ($this->presenter != $request->getPresenterName()) {
411
			return NULL;
412
		}
413
414
		$base_url = $url->getBaseUrl();
415
416
		$action = $request->getParameter('action');
417
		$parameters = $request->getParameters();
418
		unset($parameters['action']);
419
		$path = ltrim($this->getPath(), '/');
420
421
		if (FALSE === array_search($action, $this->actions)) {
422
			return NULL;
423
		}
424
425
		foreach ($parameters as $name => $value) {
426
			if (strpos($path, "<{$name}>") !== FALSE && $value !== NULL) {
427
				$path = str_replace("<{$name}>", $value, $path);
428
429
				unset($parameters[$name]);
430
			}
431
		}
432
433
		$path = preg_replace_callback('/\[.+?\]/', function($item) {
434
			if (strpos(end($item), '<')) {
435
				return '';
436
			}
437
438
			return end($item);
439
		}, $path);
440
441
		/**
442
		 * There are still some required parameters in url mask
443
		 */
444
		if (preg_match('/<\w+>/', $path)) {
445
			return NULL;
446
		}
447
448
		$path = str_replace(['[', ']'], '', $path);
449
450
		$query = http_build_query($parameters);
451
452
		return $base_url . $path . ($query ? '?' . $query : '');
453
	}
454
455
}
456