Passed
Push — master ( 50ee3e...c1f03e )
by Atanas
02:03
created

HttpKernel::getHandlerMiddleware()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 8
rs 10
ccs 6
cts 6
cp 1
crap 2
1
<?php
2
/**
3
 * @package   WPEmerge
4
 * @author    Atanas Angelov <[email protected]>
5
 * @copyright 2018 Atanas Angelov
6
 * @license   https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0
7
 * @link      https://wpemerge.com/
8
 */
9
10
namespace WPEmerge\Kernels;
11
12
use Closure;
13
use Exception;
14
use Psr\Http\Message\ResponseInterface;
15
use WPEmerge\Application\Application;
16
use WPEmerge\Application\InjectionFactory;
17
use WPEmerge\Exceptions\ConfigurationException;
18
use WPEmerge\Exceptions\ErrorHandlerInterface;
19
use WPEmerge\Helpers\Handler;
20
use WPEmerge\Helpers\HandlerFactory;
21
use WPEmerge\Middleware\HasControllerMiddlewareInterface;
22
use WPEmerge\Middleware\HasMiddlewareDefinitionsTrait;
23
use WPEmerge\Requests\RequestInterface;
24
use WPEmerge\Responses\ResponsableInterface;
25
use WPEmerge\Responses\ResponseService;
26
use WPEmerge\Routing\HasQueryFilterInterface;
27
use WPEmerge\Routing\Router;
28
use WPEmerge\Routing\SortsMiddlewareTrait;
29
30
/**
31
 * Describes how a request is handled.
32
 */
33
class HttpKernel implements HttpKernelInterface {
34
	use HasMiddlewareDefinitionsTrait;
35
	use SortsMiddlewareTrait;
36
37
	/**
38
	 * Application.
39
	 *
40
	 * @var Application
41
	 */
42
	protected $app = null;
43
44
	/**
45
	 * Injection factory.
46
	 *
47
	 * @var InjectionFactory
48
	 */
49
	protected $injection_factory = null;
50
51
	/**
52
	 * Handler factory.
53
	 *
54
	 * @var HandlerFactory
55
	 */
56
	protected $handler_factory = null;
57
58
	/**
59
	 * Response service.
60
	 *
61
	 * @var ResponseService
62
	 */
63
	protected $response_service = null;
64
65
	/**
66
	 * Request.
67
	 *
68
	 * @var RequestInterface
69
	 */
70
	protected $request = null;
71
72
	/**
73
	 * Router.
74
	 *
75
	 * @var Router
76
	 */
77
	protected $router = null;
78
79
	/**
80
	 * Error handler.
81
	 *
82
	 * @var ErrorHandlerInterface
83
	 */
84
	protected $error_handler = null;
85
86
	/**
87
	 * Constructor.
88
	 *
89
	 * @codeCoverageIgnore
90
	 * @param Application           $app
91
	 * @param InjectionFactory      $injection_factory
92
	 * @param HandlerFactory        $handler_factory
93
	 * @param ResponseService       $response_service
94
	 * @param RequestInterface      $request
95
	 * @param Router                $router
96
	 * @param ErrorHandlerInterface $error_handler
97
	 */
98
	public function __construct(
99
		Application $app,
100
		InjectionFactory $injection_factory,
101
		HandlerFactory $handler_factory,
102
		ResponseService $response_service,
103
		RequestInterface $request,
104
		Router $router,
105
		ErrorHandlerInterface $error_handler
106
	) {
107
		$this->app = $app;
108
		$this->injection_factory = $injection_factory;
109
		$this->handler_factory = $handler_factory;
110
		$this->response_service = $response_service;
111
		$this->request = $request;
112
		$this->router = $router;
113
		$this->error_handler = $error_handler;
114
	}
115
116
	/**
117
	 * {@inheritDoc}
118
	 * @codeCoverageIgnore
119
	 */
120
	public function bootstrap() {
121
		// Web. Use 3100 so it's high enough and has uncommonly used numbers
122
		// before and after. For example, 1000 is too common and it would have 999 before it
123
		// which is too common as well.).
124
		add_action( 'request', [$this, 'filterRequest'], 3100 );
125
		add_action( 'template_include', [$this, 'filterTemplateInclude'], 3100 );
126
127
		// Ajax.
128
		add_action( 'admin_init', [$this, 'registerAjaxAction'] );
129
130
		// Admin.
131
		add_action( 'admin_init', [$this, 'registerAdminAction'] );
132
	}
133
134
	/**
135
	 * Convert a user returned response to a ResponseInterface instance if possible.
136
	 * Return the original value if unsupported.
137
	 *
138
	 * @param  mixed $response
139
	 * @return mixed
140 4
	 */
141 4
	protected function toResponse( $response ) {
142 1
		if ( is_string( $response ) ) {
143
			return $this->response_service->output( $response );
144
		}
145 3
146 1
		if ( is_array( $response ) ) {
147
			return $this->response_service->json( $response );
148
		}
149 2
150 1
		if ( $response instanceof ResponsableInterface ) {
151
			return $response->toResponse();
152
		}
153 1
154
		return $response;
155
	}
156
157
	/**
158
	 * Get middleware registered with the given handler.
159
	 *
160
	 * @param  Handler       $handler
161
	 * @return array<string>
162
	 */
163 2
	protected function getHandlerMiddleware( Handler $handler ) {
164 2
		$instance = $handler->make();
165 2
166
		if ( ! $instance instanceof HasControllerMiddlewareInterface ) {
167 2
			return [];
168 1
		}
169
170 1
		return $instance->getMiddleware( $handler->get()['method'] );
171
	}
172
173
	/**
174 1
	 * Execute a handler.
175
	 *
176
	 * @param  Handler           $handler
177
	 * @param  array             $arguments
178
	 * @return ResponseInterface
179
	 */
180
	protected function executeHandler( Handler $handler, $arguments = [] ) {
181
		$response = call_user_func_array( [$handler, 'execute'], $arguments );
182
		$response = $this->toResponse( $response );
183
184
		if ( ! $response instanceof ResponseInterface ) {
185 3
			throw new ConfigurationException(
186 3
				'Response returned by controller is not valid ' .
187
				'(expected ' . ResponseInterface::class . '; received ' . gettype( $response ) . ').'
188 3
			);
189 3
		}
190
191
		return $response;
192 2
	}
193 2
194 2
	/**
195
	 * Execute an array of middleware recursively (last in, first out).
196 2
	 *
197 2
	 * @param  array<array<string>> $middleware
198 2
	 * @param  RequestInterface     $request
199 2
	 * @param  Closure              $next
200 2
	 * @return ResponseInterface
201
	 */
202
	protected function executeMiddleware( $middleware, RequestInterface $request, Closure $next ) {
203 2
		$top_middleware = array_shift( $middleware );
204
205
		if ( $top_middleware === null ) {
206
			return $next( $request );
207
		}
208
209 2
		$top_middleware_next = function ( $request ) use ( $middleware, $next ) {
210 2
			return $this->executeMiddleware( $middleware, $request, $next );
211
		};
212
213 2
		$class = $top_middleware[0];
214 2
		$instance = $this->injection_factory->make( $class );
215 2
		$arguments = array_merge(
216
			[$request, $top_middleware_next],
217 2
			array_slice( $top_middleware, 1 )
218 2
		);
219 2
220 2
		return call_user_func_array( [$instance, 'handle'], $arguments );
221 1
	}
222 1
223
	/**
224
	 * {@inheritDoc}
225 1
	 */
226
	public function run( RequestInterface $request, $middleware, $handler, $arguments = [] ) {
227 1
		$this->error_handler->register();
228
229
		try {
230
			$handler = $handler instanceof Handler ? $handler : $this->handler_factory->make( $handler );
231
232
			$middleware = array_merge( $middleware, $this->getHandlerMiddleware( $handler ) );
233 2
			$middleware = $this->expandMiddleware( $middleware );
234 2
			$middleware = $this->uniqueMiddleware( $middleware );
235
			$middleware = $this->sortMiddleware( $middleware );
236 2
237 1
			$response = $this->executeMiddleware( $middleware, $request, function () use ( $handler, $arguments ) {
238
				return $this->executeHandler( $handler, $arguments );
239
			} );
240 1
		} catch ( Exception $exception ) {
241
			$response = $this->error_handler->getResponse( $request, $exception );
242
		}
243 1
244 1
		$this->error_handler->unregister();
245
246 1
		return $response;
247 1
	}
248 1
249
	/**
250 1
	 * {@inheritDoc}
251 1
	 */
252 1
	public function handle( RequestInterface $request, $arguments = [] ) {
253 1
		$route = $this->router->execute( $request );
254 1
255 1
		if ( $route === null ) {
256 1
			return null;
257 1
		}
258
259
		$route_arguments = $route->getArguments( $request );
260
261 1
		$request = $request
262 1
			->withAttribute( 'route', $route )
263
			->withAttribute( 'route_arguments', $route_arguments );
264 1
265
		$response = $this->run(
266
			$request,
267
			$route->getMiddleware(),
268
			$route->getHandler(),
269
			array_merge(
270
				[$request],
271
				$arguments,
272
				$route_arguments
273
			)
274
		);
275
276
		$container = $this->app->getContainer();
277
		$container[ WPEMERGE_RESPONSE_KEY ] = $response;
278
279
		return $response;
280
	}
281
282
	/**
283
	 * Respond with the current response.
284
	 *
285
	 * @return void
286 2
	 */
287
	public function respond() {
288 2
		$response = $this->app->resolve( WPEMERGE_RESPONSE_KEY );
289
290 2
		if ( $response instanceof ResponseInterface ) {
291 2
			$this->response_service->respond( $response );
292 2
		}
293
	}
294
295 2
	/**
296 1
	 * Filter the main query vars.
297
	 *
298
	 * @param  array $query_vars
299 2
	 * @return array
300 2
	 */
301
	public function filterRequest( $query_vars ) {
302
		/** @var $routes \WPEmerge\Routing\RouteInterface[] */
303 2
		$routes = $this->router->getRoutes();
304
305
		foreach ( $routes as $route ) {
306
			if ( ! $route instanceof HasQueryFilterInterface ) {
307
				continue;
308
			}
309
310
			if ( ! $route->isSatisfied( $this->request ) ) {
311
				continue;
312 3
			}
313
314 3
			$query_vars = $route->applyQueryFilter( $this->request, $query_vars );
315
			break;
316 3
		}
317
318 3
		return $query_vars;
319 2
	}
320 1
321
	/**
322
	 * Filter the main template file.
323 2
	 *
324
	 * @param  string $view
325 2
	 * @return string
326
	 */
327
	public function filterTemplateInclude( $view ) {
328 1
		/** @var $wp_query \WP_Query */
329
		global $wp_query;
330
331
		$response = $this->handle( $this->request, [$view] );
332
333
		if ( $response instanceof ResponseInterface ) {
334
			if ( $response->getStatusCode() === 404 ) {
335
				$wp_query->set_404();
336
			}
337
338
			add_action( 'wpemerge.respond', [$this, 'respond'] );
339
340
			return WPEMERGE_DIR . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'view.php';
341
		}
342
343
		return $view;
344
	}
345
346
	/**
347
	 * Register ajax action to hook into current one.
348
	 *
349
	 * @return void
350
	 */
351
	public function registerAjaxAction() {
352
		if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
353
			return;
354
		}
355
356
		$action = $this->request->body( 'action', $this->request->query( 'action' ) );
357
		$action = sanitize_text_field( $action );
358
359
		add_action( "wp_ajax_{$action}", [$this, 'actionAjax'] );
360
		add_action( "wp_ajax_nopriv_{$action}", [$this, 'actionAjax'] );
361
	}
362
363
	/**
364
	 * Act on ajax action.
365
	 *
366
	 * @return void
367
	 */
368
	public function actionAjax() {
369
		$response = $this->handle( $this->request, [''] );
370
371
		if ( ! $response instanceof ResponseInterface ) {
372
			return;
373
		}
374
375
		$this->response_service->respond( $response );
376
377
		wp_die( '', '', ['response' => null] );
378
	}
379
380
	/**
381
	 * Get page hook.
382
	 * Slightly modified version of code from wp-admin/admin.php.
383
	 *
384
	 * @return string
385
	 */
386
	protected function getAdminPageHook() {
387
		global $pagenow, $typenow, $plugin_page;
388
389
		$page_hook = '';
390
391
		if ( isset( $plugin_page ) ) {
392
			$the_parent = $pagenow;
393
394
			if ( ! empty( $typenow ) ) {
395
				$the_parent = $pagenow . '?post_type=' . $typenow;
396
			}
397
398
			$page_hook = get_plugin_page_hook( $plugin_page, $the_parent );
399
		}
400
401
		return $page_hook;
402
	}
403
404
	/**
405
	 * Get admin page hook.
406
	 * Slightly modified version of code from wp-admin/admin.php.
407
	 *
408
	 * @param  string $page_hook
409
	 * @return string
410
	 */
411
	protected function getAdminHook( $page_hook ) {
412
		global $pagenow, $plugin_page;
413
414
		if ( ! empty( $page_hook ) ) {
415
			return $page_hook;
416
		}
417
418
		if ( isset( $plugin_page ) ) {
419
			return $plugin_page;
420
		}
421
422
		if ( isset( $pagenow ) ) {
423
			return $pagenow;
424
		}
425
426
		return '';
427
	}
428
429
	/**
430
	 * Register admin action to hook into current one.
431
	 *
432
	 * @return void
433
	 */
434
	public function registerAdminAction() {
435
		$page_hook = $this->getAdminPageHook();
436
		$hook_suffix = $this->getAdminHook( $page_hook );
437
438
		add_action( "load-{$hook_suffix}", [$this, 'actionAdminLoad'] );
439
		add_action( $hook_suffix, [$this, 'actionAdmin'] );
440
	}
441
442
	/**
443
	 * Act on admin action load.
444
	 *
445
	 * @return void
446
	 */
447
	public function actionAdminLoad() {
448
		$response = $this->handle( $this->request, [''] );
449
450
		if ( ! $response instanceof ResponseInterface ) {
451
			return;
452
		}
453
454
		if ( ! headers_sent() ) {
455
			$this->response_service->sendHeaders( $response );
456
		}
457
	}
458
459
	/**
460
	 * Act on admin action.
461
	 *
462
	 * @return void
463
	 */
464
	public function actionAdmin() {
465
		$response = $this->app->resolve( WPEMERGE_RESPONSE_KEY );
466
467
		if ( ! $response instanceof ResponseInterface ) {
468
			return;
469
		}
470
471
		$this->response_service->sendBody( $response );
472
	}
473
}
474