Completed
Branch master (205409)
by Timothy
02:36
created

Dispatcher::getController()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 0
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
/**
3
 * Hummingbird Anime Client
4
 *
5
 * An API client for Hummingbird to manage anime and manga watch lists
6
 *
7
 * PHP version 7
8
 *
9
 * @package     HummingbirdAnimeClient
10
 * @author      Timothy J. Warren <[email protected]>
11
 * @copyright   2015 - 2016  Timothy J. Warren
12
 * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
 * @version     3.1
14
 * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15
 */
16
17
namespace Aviat\AnimeClient;
18
19
use Aviat\Ion\Di\ContainerInterface;
20
use Aviat\Ion\Friend;
21
use GuzzleHttp\Exception\ServerException;
22
23
/**
24
 * Basic routing/ dispatch
25
 */
26
class Dispatcher extends RoutingBase {
27
28
	/**
29
	 * The route-matching object
30
	 * @var object $router
31
	 */
32
	protected $router;
33
34
	/**
35
	 * The route matcher
36
	 * @var object $matcher
37
	 */
38
	protected $matcher;
39
40
	/**
41
	 * Class wrapper for input superglobals
42
	 * @var Psr\Http\Message\ServerRequestInterface
43
	 */
44
	protected $request;
45
46
	/**
47
	 * Routes added to router
48
	 * @var array $output_routes
49
	 */
50
	protected $output_routes;
51
52
	/**
53
	 * Constructor
54
	 *
55
	 * @param ContainerInterface $container
56
	 */
57
	public function __construct(ContainerInterface $container)
58
	{
59
		parent::__construct($container);
60
		$this->router = $container->get('aura-router')->getMap();
61
		$this->matcher = $container->get('aura-router')->getMatcher();
62
		$this->request = $container->get('request');
63
64
		$this->output_routes = $this->_setupRoutes();
65
	}
66
67
	/**
68
	 * Get the current route object, if one matches
69
	 *
70
	 * @return object
71
	 */
72
	public function getRoute()
73
	{
74
		$logger = $this->container->getLogger('default');
75
76
		$raw_route = $this->request->getUri()->getPath();
77
		$route_path = "/" . trim($raw_route, '/');
78
79
		$logger->info('Dispatcher - Routing data from get_route method');
80
		$logger->info(print_r([
81
			'route_path' => $route_path
82
		], TRUE));
83
84
		return $this->matcher->match($this->request);
85
	}
86
87
	/**
88
	 * Get list of routes applied
89
	 *
90
	 * @return array
91
	 */
92
	public function getOutputRoutes()
93
	{
94
		return $this->output_routes;
95
	}
96
97
	/**
98
	 * Handle the current route
99
	 *
100
	 * @codeCoverageIgnore
101
	 * @param object|null $route
102
	 * @return void
103
	 */
104
	public function __invoke($route = NULL)
105
	{
106
		$logger = $this->container->getLogger('default');
107
108
		if (is_null($route))
109
		{
110
			$route = $this->getRoute();
111
112
			$logger->info('Dispatcher - Route invoke arguments');
113
			$logger->info(print_r($route, TRUE));
114
		}
115
116
		if ($route)
117
		{
118
			$parsed = $this->processRoute(new Friend($route));
0 ignored issues
show
Documentation introduced by
new \Aviat\Ion\Friend($route) is of type object<Aviat\Ion\Friend>, but the function expects a object<Aura\Router\Route>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
119
			$controllerName = $parsed['controller_name'];
120
			$actionMethod = $parsed['action_method'];
121
			$params = $parsed['params'];
122
		}
123
		else
124
		{
125
			// If not route was matched, return an appropriate http
126
			// error message
127
			$error_route = $this->getErrorParams();
128
			$controllerName = AnimeClient::DEFAULT_CONTROLLER;
129
			$actionMethod = $error_route['action_method'];
130
			$params = $error_route['params'];
131
		}
132
		
133
		// Try to catch API errors in a presentable fashion
134
		try
135
		{
136
			// Actually instantiate the controller
137
			$this->call($controllerName, $actionMethod, $params);
138
		}
139
		catch (ServerException $e)
140
		{
141
			$response = $e->getResponse();
142
			$this->call(AnimeClient::DEFAULT_CONTROLLER, AnimeClient::ERROR_MESSAGE_METHOD, [
143
				$response->getStatusCode(),
144
				'API Error',
145
				'There was a problem getting data from an external source.',
146
				(string) $response->getBody()
147
			]);
148
		}
149
	}
150
151
	/**
152
	 * Parse out the arguments for the appropriate controller for
153
	 * the current route
154
	 *
155
	 * @param \Aura\Router\Route $route
156
	 * @throws \LogicException
157
	 * @return array
158
	 */
159
	protected function processRoute($route)
160
	{
161
		if (array_key_exists('controller', $route->attributes))
162
		{
163
			$controller_name = $route->attributes['controller'];
164
		}
165
		else
166
		{
167
			throw new \LogicException("Missing controller");
168
		}
169
170
		// Get the full namespace for a controller if a short name is given
171
		if (strpos($controller_name, '\\') === FALSE)
172
		{
173
			$map = $this->getControllerList();
174
			$controller_name = $map[$controller_name];
175
		}
176
177
		$action_method = (array_key_exists('action', $route->attributes))
178
			? $route->attributes['action']
179
			: AnimeClient::NOT_FOUND_METHOD;
180
181
		$params = [];
182
		if ( ! empty($route->__get('tokens')))
183
		{
184
			$tokens = array_keys($route->__get('tokens'));
185
			foreach ($tokens as $param)
186
			{
187
				if (array_key_exists($param, $route->attributes))
188
				{
189
					$params[$param] = $route->attributes[$param];
190
				}
191
			}
192
		}
193
		$logger = $this->container->getLogger('default');
194
		$logger->info(json_encode($params));
195
196
		return [
197
			'controller_name' => $controller_name,
198
			'action_method' => $action_method,
199
			'params' => $params
200
		];
201
	}
202
203
	/**
204
	 * Get the type of route, to select the current controller
205
	 *
206
	 * @return string
207
	 */
208
	public function getController()
209
	{
210
		$route_type = $this->__get('default_list');
211
		$request_uri = $this->request->getUri()->getPath();
212
		$path = trim($request_uri, '/');
213
214
		$segments = explode('/', $path);
215
		$controller = reset($segments);
216
217
		if (empty($controller))
218
		{
219
			$controller = $route_type;
220
		}
221
222
		return $controller;
223
	}
224
225
	/**
226
	 * Get the list of controllers in the default namespace
227
	 *
228
	 * @return array
229
	 */
230
	public function getControllerList()
231
	{
232
		$default_namespace = AnimeClient::DEFAULT_CONTROLLER_NAMESPACE;
233
		$path = str_replace('\\', '/', $default_namespace);
234
		$path = str_replace('Aviat/AnimeClient/', '', $path);
235
		$path = trim($path, '/');
236
		$actual_path = realpath(_dir(AnimeClient::SRC_DIR, $path));
237
		$class_files = glob("{$actual_path}/*.php");
238
239
		$controllers = [];
240
241
		foreach ($class_files as $file)
242
		{
243
			$raw_class_name = basename(str_replace(".php", "", $file));
244
			$path = strtolower(basename($raw_class_name));
245
			$class_name = trim($default_namespace . '\\' . $raw_class_name, '\\');
246
247
			$controllers[$path] = $class_name;
248
		}
249
250
		return $controllers;
251
	}
252
253
	/**
254
	 * Create the controller object and call the appropriate
255
	 * method
256
	 *
257
	 * @param  string $controllerName - The full namespace of the controller class
258
	 * @param  string $method
259
	 * @param  array  $params
260
	 * @return void
261
	 */
262
	protected function call($controllerName, $method, array $params)
263
	{
264
		$logger = $this->container->getLogger('default');
265
266
		$controller = new $controllerName($this->container);
267
268
		// Run the appropriate controller method
269
		$logger->debug('Dispatcher - controller arguments');
270
		$logger->debug(print_r($params, TRUE));
271
		call_user_func_array([$controller, $method], $params);
272
	}
273
274
	/**
275
	 * Get the appropriate params for the error page
276
	 * pased on the failed route
277
	 *
278
	 * @return array|false
279
	 */
280
	protected function getErrorParams()
281
	{
282
		$logger = $this->container->getLogger('default');
283
		$failure = $this->matcher->getFailedRoute();
284
285
		$logger->info('Dispatcher - failed route');
286
		$logger->info(print_r($failure, TRUE));
287
288
		$action_method = AnimeClient::ERROR_MESSAGE_METHOD;
289
290
		$params = [];
291
292
		switch($failure->failedRule) {
293
			case 'Aura\Router\Rule\Allows':
294
				$params = [
295
					'http_code' => 405,
296
					'title' => '405 Method Not Allowed',
297
					'message' => 'Invalid HTTP Verb'
298
				];
299
			break;
300
301
			case 'Aura\Router\Rule\Accepts':
302
				$params = [
303
					'http_code' => 406,
304
					'title' => '406 Not Acceptable',
305
					'message' => 'Unacceptable content type'
306
				];
307
			break;
308
309
			default:
310
				// Fall back to a 404 message
311
				$action_method = AnimeClient::NOT_FOUND_METHOD;
312
			break;
313
		}
314
315
		return [
316
			'params' => $params,
317
			'action_method' => $action_method
318
		];
319
	}
320
321
	/**
322
	 * Select controller based on the current url, and apply its relevent routes
323
	 *
324
	 * @return array
325
	 */
326
	protected function _setupRoutes()
327
	{
328
		$route_type = $this->getController();
329
330
		// Add routes
331
		$routes = [];
332
		foreach ($this->routes as $name => &$route)
333
		{
334
			$path = $route['path'];
335
			unset($route['path']);
336
337
			$controller_map = $this->getControllerList();
338
			$controller_class = (array_key_exists($route_type, $controller_map))
339
				? $controller_map[$route_type]
340
				: AnimeClient::DEFAULT_CONTROLLER;
341
342
			if (array_key_exists($route_type, $controller_map))
343
			{
344
				$controller_class = $controller_map[$route_type];
345
			}
346
347
			// Prepend the controller to the route parameters
348
			$route['controller'] = $controller_class;
349
350
			// Select the appropriate router method based on the http verb
351
			$add = (array_key_exists('verb', $route))
352
				? strtolower($route['verb'])
353
				: "get";
354
355
			// Add the route to the router object
356
			if ( ! array_key_exists('tokens', $route))
357
			{
358
				$routes[] = $this->router->$add($name, $path)->defaults($route);
359
			}
360
			else
361
			{
362
				$tokens = $route['tokens'];
363
				unset($route['tokens']);
364
365
				$routes[] = $this->router->$add($name, $path)
366
					->defaults($route)
367
					->tokens($tokens);
368
			}
369
		}
370
371
		return $routes;
372
	}
373
}
374
// End of Dispatcher.php