Completed
Push — master ( c1b03c...214a55 )
by Nazar
04:08
created

Router::controller_router_available_methods()   C

Complexity

Conditions 7
Paths 4

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 24
rs 6.7272
cc 7
eloc 17
nc 4
nop 3
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
	cli,
11
	cs\Config,
12
	cs\Config\Module_Properties,
13
	cs\ExitException,
14
	cs\Page,
15
	cs\Request,
16
	cs\Response;
17
18
/**
19
 * @property string[] $controller_path Path that will be used by controller to render page
20
 */
21
trait Router {
22
	/**
23
	 * Path that will be used by controller to render page
24
	 *
25
	 * @var string[]
26
	 */
27
	protected $controller_path;
28
	/**
29
	 * Execute router
30
	 *
31
	 * Depending on module, files-based or controller-based router might be used
32
	 *
33
	 * @throws ExitException
34
	 */
35
	protected function execute_router () {
36
		$Request = Request::instance();
37
		$this->check_and_normalize_route($Request);
38
		if ($Request->method == 'CLI') {
39
			$this->print_cli_structure($Request->path);
40
			return;
41
		}
42
		if (file_exists("$this->working_directory/Controller.php")) {
43
			$this->controller_router($Request);
44
		} else {
45
			$this->files_router($Request);
46
		}
47
	}
48
	protected function print_cli_structure ($path) {
49
		$Config = Config::instance();
50
		$result = [];
51
		foreach ($Config->components['modules'] as $module_name => $data) {
52
			if ($data['active'] == Module_Properties::ENABLED) {
53
				$working_dir = MODULES."/$module_name/cli";
54
				$structure   = file_exists("$working_dir/index.json") ? file_get_json("$working_dir/index.json") : [];
55
				$this->print_cli_structure_internal(
56
					$working_dir,
57
					$module_name,
58
					'',
59
					$structure,
60
					$result[$module_name]
61
				);
62
			}
63
		}
64
		$result = $this->print_cli_structure_normalize_result($result);
65
		$Page   = Page::instance();
66
		// Cut `/cli/` prefix
67
		$path = substr($path, 5);
68
		if ($path) {
69
			$Page->content("%WPaths and methods for \"$path\":%n\n");
70
			$result = array_filter(
71
				$result,
72
				function ($item) use ($path) {
73
					return strpos($item[0], $path) === 0;
74
				}
75
			);
76
		} else {
77
			$Page->content("%WAll paths and methods:%n\n");
78
		}
79
		$Page->content(
80
			implode("\n", (new cli\Table(['Path', 'Methods available'], $result))->getDisplayLines())."\n"
81
		);
82
	}
83
	/**
84
	 * @param string $dir
85
	 * @param string $module_name
86
	 * @param string $basename
87
	 * @param array  $structure
88
	 * @param array  $result
89
	 */
90
	protected function print_cli_structure_internal ($dir, $module_name, $basename, $structure, &$result) {
91
		/** @noinspection NestedTernaryOperatorInspection */
92
		foreach ($structure ?: (!$basename ? ['index'] : []) as $path => $nested_structure) {
93
			if (!is_array($nested_structure)) {
94
				$path             = $nested_structure;
95
				$nested_structure = [];
96
			}
97
			$key = $path == '_' ? 0 : $path;
98
			if (file_exists("$dir/Controller.php")) {
99
				$result[$key] = $this->controller_router_available_methods(
100
					$dir,
101
					"\\cs\\modules\\$module_name\\cli\\Controller",
102
					$basename ? $basename.'_'.$path : $path
103
				);
104
				if ($structure && $nested_structure) {
105
					$this->print_cli_structure_internal($dir, $module_name, $basename ? $basename.'_'.$path : $path, $nested_structure, $result[$key]);
106
				}
107
			} else {
108
				$result[$key] = $this->files_router_available_methods($dir, $path);
109
				if ($structure && $nested_structure) {
110
					$this->print_cli_structure_internal("$dir/$path", $module_name, $basename, $nested_structure, $result[$key]);
111
				}
112
			}
113
		}
114
	}
115
	/**
116
	 * @param array  $result
117
	 * @param string $prefix
118
	 *
119
	 * @return string[]
120
	 */
121
	protected function print_cli_structure_normalize_result ($result, $prefix = '') {
122
		$normalized = [];
123
		foreach ($result as $key => $value) {
124
			if (is_array_assoc($value)) {
125
				if (!$prefix && isset($value['index'])) {
126
					$value[0] = $value['index'];
127
					unset($value['index']);
128
				}
129
				if (is_array(@$value[0]) && $value[0]) {
130
					$normalized[] = [$prefix.$key, strtolower(implode(', ', $value[0]))];
131
				}
132
				unset($value[0]);
133
				/** @noinspection SlowArrayOperationsInLoopInspection */
134
				$normalized = array_merge($normalized, $this->print_cli_structure_normalize_result($value, $prefix.$key.'/'));
135
			} elseif (is_array($value) && $value) {
136
				$normalized[] = [$prefix.$key, strtolower(implode(', ', $value))];
137
			}
138
		}
139
		return $normalized;
140
	}
141
	/**
142
	 * Normalize `cs\Request::$route_path` and fill `cs\App::$controller_path`
143
	 *
144
	 * @param Request $Request
145
	 *
146
	 * @throws ExitException
147
	 */
148
	protected function check_and_normalize_route ($Request) {
149
		if (!file_exists("$this->working_directory/index.json")) {
150
			return;
151
		}
152
		$structure = file_get_json("$this->working_directory/index.json");
153
		if (!$structure) {
154
			return;
155
		}
156
		for ($nesting_level = 0; $structure; ++$nesting_level) {
157
			/**
158
			 * Next level of routing path
159
			 */
160
			$path = @$Request->route_path[$nesting_level];
161
			/**
162
			 * If path not specified - take first from structure
163
			 */
164
			$this->check_and_normalize_route_internal($path, $structure, $Request->cli_path || $Request->api_path);
165
			$Request->route_path[$nesting_level] = $path;
166
			/**
167
			 * Fill paths array intended for controller's usage
168
			 */
169
			$this->controller_path[] = $path;
170
			/**
171
			 * If nested structure is not available - we'll not go into next iteration of this cycle
172
			 */
173
			$structure = @$structure[$path];
174
		}
175
	}
176
	/**
177
	 * @param string $path
178
	 * @param array  $structure
179
	 * @param bool   $cli_or_api_path
180
	 *
181
	 * @throws ExitException
182
	 */
183
	protected function check_and_normalize_route_internal (&$path, $structure, $cli_or_api_path) {
184
		/**
185
		 * If path not specified - take first from structure
186
		 */
187
		if (!$path) {
188
			$path = isset($structure[0]) ? $structure[0] : array_keys($structure)[0];
189
			/**
190
			 * We need exact paths for CLI and API request (or `_` ending if available) and less strict mode for other cases that allows go deeper automatically
191
			 */
192
			if ($path !== '_' && $cli_or_api_path) {
193
				throw new ExitException(404);
194
			}
195
		} elseif (!isset($structure[$path]) && !in_array($path, $structure)) {
196
			throw new ExitException(404);
197
		}
198
		/** @noinspection PhpUndefinedMethodInspection */
199
		if (!$this->check_permission($path)) {
200
			throw new ExitException(403);
201
		}
202
	}
203
	/**
204
	 * Include files necessary for module page rendering
205
	 *
206
	 * @param Request $Request
207
	 *
208
	 * @throws ExitException
209
	 */
210
	protected function files_router ($Request) {
211
		foreach ($this->controller_path as $index => $path) {
212
			/**
213
			 * Starting from index 2 we need to maintain slash-separated string that includes all paths from index 1 and till current
214
			 */
215
			if ($index > 1) {
216
				$path = implode('/', array_slice($this->controller_path, 1, $index));
217
			}
218
			$next_exists = isset($this->controller_path[$index + 1]);
219
			$this->files_router_handler($Request, $this->working_directory, $path, !$next_exists);
220
		}
221
	}
222
	/**
223
	 * Include files that corresponds for specific paths in URL
224
	 *
225
	 * @param Request $Request
226
	 * @param string  $dir
227
	 * @param string  $basename
228
	 * @param bool    $required
229
	 *
230
	 * @throws ExitException
231
	 */
232
	protected function files_router_handler ($Request, $dir, $basename, $required = true) {
233
		$this->files_router_handler_internal($Request, $dir, $basename, $required);
234
	}
235
	/**
236
	 * @param Request $Request
237
	 * @param string  $dir
238
	 * @param string  $basename
239
	 * @param bool    $required
240
	 *
241
	 * @throws ExitException
242
	 */
243
	protected function files_router_handler_internal ($Request, $dir, $basename, $required) {
244
		$included = _include("$dir/$basename.php", false, false) !== false;
245
		if (!$Request->cli_path && !$Request->api_path) {
246
			return;
247
		}
248
		$request_method = strtolower($Request->method);
249
		$included       = _include("$dir/$basename.$request_method.php", false, false) !== false || $included;
250
		if ($included || !$required) {
251
			return;
252
		}
253
		$this->handler_not_found(
254
			$this->files_router_available_methods($dir, $basename),
255
			$request_method,
256
			$Request
257
		);
258
	}
259
	/**
260
	 * @param string $dir
261
	 * @param string $basename
262
	 *
263
	 * @return string[]
264
	 */
265
	protected function files_router_available_methods ($dir, $basename) {
266
		$methods = get_files_list($dir, "/^$basename\\.[a-z]+\\.php$/");
267
		$methods = _strtoupper(_substr($methods, strlen($basename) + 1, -4));
268
		natcasesort($methods);
269
		return array_values($methods);
270
	}
271
	/**
272
	 * 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
273
	 * methods also doesn't exist
274
	 *
275
	 * @param string[] $available_methods
276
	 * @param string   $request_method
277
	 * @param Request  $Request
278
	 *
279
	 * @throws ExitException
280
	 */
281
	protected function handler_not_found ($available_methods, $request_method, $Request) {
282
		if ($available_methods) {
283
			if ($Request->cli_path) {
284
				$this->print_cli_structure($Request->path);
285
				throw new ExitException(501);
286
			} else {
287
				Response::instance()->header('Allow', implode(', ', $available_methods));
288
				if ($request_method !== 'options') {
289
					throw new ExitException(501);
290
				}
291
			}
292
		} else {
293
			throw new ExitException(404);
294
		}
295
	}
296
	/**
297
	 * Call methods necessary for module page rendering
298
	 *
299
	 * @param Request $Request
300
	 *
301
	 * @throws ExitException
302
	 */
303
	protected function controller_router ($Request) {
304
		$suffix = '';
305
		if ($Request->cli_path) {
306
			$suffix = '\\cli';
307
		} elseif ($Request->admin_path) {
308
			$suffix = '\\admin';
309
		} elseif ($Request->api_path) {
310
			$suffix = '\\api';
311
		}
312
		$controller_class = "cs\\modules\\$Request->current_module$suffix\\Controller";
313
		foreach ($this->controller_path as $index => $path) {
314
			/**
315
			 * Starting from index 2 we need to maintain underscore-separated string that includes all paths from index 1 and till current
316
			 */
317
			if ($index > 1) {
318
				$path = implode('_', array_slice($this->controller_path, 1, $index));
319
			}
320
			$next_exists = isset($this->controller_path[$index + 1]);
321
			$this->controller_router_handler($Request, $controller_class, $path, !$next_exists);
322
		}
323
	}
324
	/**
325
	 * Call methods that corresponds for specific paths in URL
326
	 *
327
	 * @param Request $Request
328
	 * @param string  $controller_class
329
	 * @param string  $method_name
330
	 * @param bool    $required
331
	 *
332
	 * @throws ExitException
333
	 */
334
	protected function controller_router_handler ($Request, $controller_class, $method_name, $required = true) {
335
		$method_name = str_replace('.', '_', $method_name);
336
		$this->controller_router_handler_internal($Request, $controller_class, $method_name, $required);
337
	}
338
	/**
339
	 * @param Request $Request
340
	 * @param string  $controller_class
341
	 * @param string  $method_name
342
	 * @param bool    $required
343
	 *
344
	 * @throws ExitException
345
	 */
346
	protected function controller_router_handler_internal ($Request, $controller_class, $method_name, $required) {
347
		$Response = Response::instance();
348
		$found    = $this->controller_router_handler_internal_execute($controller_class, $method_name, $Request, $Response);
349
		if (!$Request->cli_path && !$Request->api_path) {
350
			return;
351
		}
352
		$request_method = strtolower($Request->method);
353
		$found          = $this->controller_router_handler_internal_execute($controller_class, $method_name.'_'.$request_method, $Request, $Response) || $found;
354
		if ($found || !$required) {
355
			return;
356
		}
357
		$this->handler_not_found(
358
			$this->controller_router_available_methods($this->working_directory, $controller_class, $method_name),
359
			$request_method,
360
			$Request
361
		);
362
	}
363
	/**
364
	 * @param string $working_directory
365
	 * @param string $controller_class
366
	 * @param string $method_name
367
	 *
368
	 * @return string[]
369
	 */
370
	protected function controller_router_available_methods ($working_directory, $controller_class, $method_name) {
371
		$structure = file_exists("$working_directory/index.json") ? file_get_json("$working_directory/index.json") : ['index'];
372
		$structure = $this->controller_router_available_methods_to_flat_structure($structure);
373
		$methods   = array_filter(
374
			get_class_methods($controller_class),
375
			function ($found_method) use ($method_name, $structure) {
376
				if (!preg_match("/^{$method_name}_[a-z_]+$/", $found_method)) {
377
					return false;
378
				}
379
				foreach ($structure as $structure_method) {
380
					if (strpos($found_method, $structure_method) === 0 && strpos($method_name, $structure_method) !== 0) {
381
						return false;
382
					}
383
				}
384
				return true;
385
			}
386
		);
387
		if (method_exists($controller_class, $method_name)) {
388
			$methods[] = $method_name;
389
		}
390
		$methods = _strtoupper(_substr($methods, strlen($method_name) + 1));
391
		natcasesort($methods);
392
		return array_values($methods);
393
	}
394
	/**
395
	 * @param array  $structure
396
	 * @param string $prefix
397
	 *
398
	 * @return string[]
399
	 */
400
	protected function controller_router_available_methods_to_flat_structure ($structure, $prefix = '') {
401
		$flat_structure = [];
402
		foreach ($structure as $path => $nested_structure) {
403
			if (is_array($nested_structure)) {
404
				$flat_structure[] = $prefix.$path;
405
				/** @noinspection SlowArrayOperationsInLoopInspection */
406
				$flat_structure = array_merge(
407
					$flat_structure,
408
					$this->controller_router_available_methods_to_flat_structure($nested_structure, $prefix.$path.'_')
409
				);
410
			} else {
411
				$flat_structure[] = $prefix.$nested_structure;
412
			}
413
		}
414
		return $flat_structure;
415
	}
416
	/**
417
	 * @param string   $controller_class
418
	 * @param string   $method_name
419
	 * @param Request  $Request
420
	 * @param Response $Response
421
	 *
422
	 * @return bool
423
	 */
424
	protected function controller_router_handler_internal_execute ($controller_class, $method_name, $Request, $Response) {
425
		if (!method_exists($controller_class, $method_name)) {
426
			return false;
427
		}
428
		$result = $controller_class::$method_name($Request, $Response);
429
		if ($result !== null) {
430
			Page::instance()->{$Request->api_path ? 'json' : 'content'}($result);
431
		}
432
		return true;
433
	}
434
}
435