Passed
Push — master ( 877150...53e563 )
by Atanas
07:02
created

HttpKernel::compose()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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