Completed
Push — master ( 2f4e66...399dca )
by Nazar
05:44
created

Router   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 245
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 3
Bugs 0 Features 2
Metric Value
wmc 41
c 3
b 0
f 2
lcom 1
cbo 4
dl 0
loc 245
rs 8.2769

11 Methods

Rating   Name   Duplication   Size   Complexity  
A execute_router() 0 9 2
B check_and_normalize_route() 0 28 4
B check_and_normalize_route_internal() 0 20 8
A files_router() 0 12 3
A files_router_handler() 0 3 1
B files_router_handler_internal() 0 14 5
A handler_not_found() 0 10 3
B controller_router() 0 19 5
A controller_router_handler() 0 4 1
B controller_router_handler_internal() 0 20 5
A controller_router_handler_internal_execute() 0 10 4

How to fix   Complexity   

Complex Class

Complex classes like Router 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 Router, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package   CleverStyle CMS
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2015-2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs\App;
9
use
10
	cs\ExitException,
11
	cs\Page,
12
	cs\Request,
13
	cs\Response;
14
15
/**
16
 * @property string[] $controller_path Path that will be used by controller to render page
17
 */
18
trait Router {
19
	/**
20
	 * Path that will be used by controller to render page
21
	 *
22
	 * @var string[]
23
	 */
24
	protected $controller_path;
25
	/**
26
	 * Execute router
27
	 *
28
	 * Depending on module, files-based or controller-based router might be used
29
	 *
30
	 * @throws ExitException
31
	 */
32
	protected function execute_router () {
33
		$Request = Request::instance();
34
		$this->check_and_normalize_route($Request);
35
		if (file_exists("$this->working_directory/Controller.php")) {
36
			$this->controller_router($Request);
37
		} else {
38
			$this->files_router($Request);
39
		}
40
	}
41
	/**
42
	 * Normalize `cs\Request::$route_path` and fill `cs\App::$controller_path`
43
	 *
44
	 * @param Request $Request
45
	 *
46
	 * @throws ExitException
47
	 */
48
	protected function check_and_normalize_route ($Request) {
49
		if (!file_exists("$this->working_directory/index.json")) {
50
			return;
51
		}
52
		$structure = file_get_json("$this->working_directory/index.json");
53
		if (!$structure) {
54
			return;
55
		}
56
		for ($nesting_level = 0; $structure; ++$nesting_level) {
57
			/**
58
			 * Next level of routing path
59
			 */
60
			$path = @$Request->route_path[$nesting_level];
61
			/**
62
			 * If path not specified - take first from structure
63
			 */
64
			$this->check_and_normalize_route_internal($path, $structure, $Request->api_path);
65
			$Request->route_path[$nesting_level] = $path;
66
			/**
67
			 * Fill paths array intended for controller's usage
68
			 */
69
			$this->controller_path[] = $path;
70
			/**
71
			 * If nested structure is not available - we'll not go into next iteration of this cycle
72
			 */
73
			$structure = @$structure[$path];
74
		}
75
	}
76
	/**
77
	 * @param string $path
78
	 * @param array  $structure
79
	 * @param bool   $api_path
80
	 *
81
	 * @throws ExitException
82
	 */
83
	protected function check_and_normalize_route_internal (&$path, $structure, $api_path) {
84
		/**
85
		 * If path not specified - take first from structure
86
		 */
87
		if (!$path) {
88
			$path = isset($structure[0]) ? $structure[0] : array_keys($structure)[0];
89
			/**
90
			 * We need exact paths for API request (or `_` ending if available) and less strict mode for other cases that allows go deeper automatically
91
			 */
92
			if ($path !== '_' && $api_path) {
93
				throw new ExitException(404);
94
			}
95
		} elseif (!isset($structure[$path]) && !in_array($path, $structure)) {
96
			throw new ExitException(404);
97
		}
98
		/** @noinspection PhpUndefinedMethodInspection */
99
		if (!$this->check_permission($path)) {
100
			throw new ExitException(403);
101
		}
102
	}
103
	/**
104
	 * Include files necessary for module page rendering
105
	 *
106
	 * @param Request $Request
107
	 *
108
	 * @throws ExitException
109
	 */
110
	protected function files_router ($Request) {
111
		foreach ($this->controller_path as $index => $path) {
112
			/**
113
			 * Starting from index 2 we need to maintain slash-separated string that includes all paths from index 1 and till current
114
			 */
115
			if ($index > 1) {
116
				$path = implode('/', array_slice($this->controller_path, 1, $index));
117
			}
118
			$next_exists = isset($this->controller_path[$index + 1]);
119
			$this->files_router_handler($Request, $this->working_directory, $path, !$next_exists);
120
		}
121
	}
122
	/**
123
	 * Include files that corresponds for specific paths in URL
124
	 *
125
	 * @param Request $Request
126
	 * @param string  $dir
127
	 * @param string  $basename
128
	 * @param bool    $required
129
	 *
130
	 * @throws ExitException
131
	 */
132
	protected function files_router_handler ($Request, $dir, $basename, $required = true) {
133
		$this->files_router_handler_internal($Request, $dir, $basename, $required);
134
	}
135
	/**
136
	 * @param Request $Request
137
	 * @param string  $dir
138
	 * @param string  $basename
139
	 * @param bool    $required
140
	 *
141
	 * @throws ExitException
142
	 */
143
	protected function files_router_handler_internal ($Request, $dir, $basename, $required) {
144
		$included = _include("$dir/$basename.php", false, false) !== false;
145
		if (!$Request->api_path) {
146
			return;
147
		}
148
		$request_method = strtolower($Request->method);
149
		$included       = _include("$dir/$basename.$request_method.php", false, false) !== false || $included;
150
		if ($included || !$required) {
151
			return;
152
		}
153
		$methods = get_files_list($dir, "/^$basename\\.[a-z]+\\.php$/");
154
		$methods = _strtoupper(_substr($methods, strlen($basename) + 1, -4));
155
		$this->handler_not_found($methods, $request_method);
156
	}
157
	/**
158
	 * If HTTP method handler not found we generate either `501 Not Implemented` if other methods are supported or `404 Not Found` if handlers for others
159
	 * methods also doesn't exist
160
	 *
161
	 * @param string[] $available_methods
162
	 * @param string   $request_method
163
	 *
164
	 * @throws ExitException
165
	 */
166
	protected function handler_not_found ($available_methods, $request_method) {
167
		if ($available_methods) {
168
			Response::instance()->header('Allow', implode(', ', $available_methods));
169
			if ($request_method !== 'options') {
170
				throw new ExitException(501);
171
			}
172
		} else {
173
			throw new ExitException(404);
174
		}
175
	}
176
	/**
177
	 * Call methods necessary for module page rendering
178
	 *
179
	 * @param Request $Request
180
	 *
181
	 * @throws ExitException
182
	 */
183
	protected function controller_router ($Request) {
184
		$suffix = '';
185
		if ($Request->admin_path) {
186
			$suffix = '\\admin';
187
		} elseif ($Request->api_path) {
188
			$suffix = '\\api';
189
		}
190
		$controller_class = "cs\\modules\\$Request->current_module$suffix\\Controller";
191
		foreach ($this->controller_path as $index => $path) {
192
			/**
193
			 * Starting from index 2 we need to maintain underscore-separated string that includes all paths from index 1 and till current
194
			 */
195
			if ($index > 1) {
196
				$path = implode('_', array_slice($this->controller_path, 1, $index));
197
			}
198
			$next_exists = isset($this->controller_path[$index + 1]);
199
			$this->controller_router_handler($Request, $controller_class, $path, !$next_exists);
200
		}
201
	}
202
	/**
203
	 * Call methods that corresponds for specific paths in URL
204
	 *
205
	 * @param Request $Request
206
	 * @param string  $controller_class
207
	 * @param string  $method_name
208
	 * @param bool    $required
209
	 *
210
	 * @throws ExitException
211
	 */
212
	protected function controller_router_handler ($Request, $controller_class, $method_name, $required = true) {
213
		$method_name = str_replace('.', '_', $method_name);
214
		$this->controller_router_handler_internal($Request, $controller_class, $method_name, $required);
215
	}
216
	/**
217
	 * @param Request $Request
218
	 * @param string  $controller_class
219
	 * @param string  $method_name
220
	 * @param bool    $required
221
	 *
222
	 * @throws ExitException
223
	 */
224
	protected function controller_router_handler_internal ($Request, $controller_class, $method_name, $required) {
225
		$Response = Response::instance();
226
		$found    = $this->controller_router_handler_internal_execute($controller_class, $method_name, $Request, $Response);
227
		if (!$Request->api_path) {
228
			return;
229
		}
230
		$request_method = strtolower($Request->method);
231
		$found          = $this->controller_router_handler_internal_execute($controller_class, $method_name.'_'.$request_method, $Request, $Response) || $found;
232
		if ($found || !$required) {
233
			return;
234
		}
235
		$methods = array_filter(
236
			get_class_methods($controller_class),
237
			function ($method) use ($method_name) {
238
				return preg_match("/^{$method_name}_[a-z]+$/", $method);
239
			}
240
		);
241
		$methods = _strtoupper(_substr($methods, strlen($method_name) + 1));
242
		$this->handler_not_found($methods, $request_method);
243
	}
244
	/**
245
	 * @param string   $controller_class
246
	 * @param string   $method_name
247
	 * @param Request  $Request
248
	 * @param Response $Response
249
	 *
250
	 * @return bool
251
	 */
252
	protected function controller_router_handler_internal_execute ($controller_class, $method_name, $Request, $Response) {
253
		if (!method_exists($controller_class, $method_name)) {
254
			return false;
255
		}
256
		$result = $controller_class::$method_name($Request, $Response);
257
		if ($result !== null) {
258
			Page::instance()->{$Request->api_path ? 'json' : 'content'}($result);
259
		}
260
		return true;
261
	}
262
}
263