Passed
Push — master ( 33655b...f08be5 )
by Atanas
02:10
created

HttpKernel::compose()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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