Passed
Push — master ( 669eba...63d013 )
by Atanas
03:06
created

HttpKernel::executeHandler()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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