Passed
Branch refactor/kernels (d3615b)
by Atanas
01:46
created

Router::expandMiddlewareGroup()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 3
nc 2
nop 1
dl 0
loc 6
ccs 0
cts 4
cp 0
c 0
b 0
f 0
cc 2
crap 6
rs 10
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\Routing;
11
12
use Psr\Http\Message\ResponseInterface;
13
use WPEmerge\Exceptions\ConfigurationException;
14
use WPEmerge\Middleware\MiddlewareInterface;
15
use WPEmerge\Requests\RequestInterface;
16
use WPEmerge\Routing\Conditions\ConditionFactory;
17
use WPEmerge\Routing\Conditions\ConditionInterface;
18
use WPEmerge\Routing\Conditions\HasUrlWhereInterface;
19
use WPEmerge\Routing\Conditions\InvalidRouteConditionException;
20
use WPEmerge\Support\Arr;
21
22
/**
23
 * Provide routing for site requests (i.e. all non-api requests).
24
 */
25
class Router implements HasRoutesInterface {
26
	use HasRoutesTrait {
27
		addRoute as traitAddRoute;
28
	}
29
30
	/**
31
	 * Condition factory.
32
	 *
33
	 * @var ConditionFactory
34
	 */
35
	protected $condition_factory = null;
36
37
	/**
38
	 * Middleware available to the application.
39
	 *
40
	 * @var array
41
	 */
42
	protected $middleware = [];
43
44
	/**
45
	 * Middleware groups.
46
	 *
47
	 * @var array<string, array>
48
	 */
49
	protected $middleware_groups = [];
50
51
	/**
52
	 * Global middleware that will be applied to all routes.
53
	 *
54
	 * @var array
55
	 */
56
	protected $global_middleware = [];
57
58
	/**
59
	 * Middleware sorted in order of execution.
60
	 *
61
	 * @var array<string>
62
	 */
63
	protected $middleware_priority = [];
64
65
	/**
66
	 * Current active route.
67
	 *
68
	 * @var RouteInterface
69
	 */
70
	protected $current_route = null;
71
72
	/**
73
	 * Group stack.
74
	 *
75
	 * @var array<array<string, mixed>>
76
	 */
77
	protected $group_stack = [];
78
79
	/**
80
	 * Constructor.
81
	 *
82
	 * @codeCoverageIgnore
83
	 * @param ConditionFactory      $condition_factory
84
	 */
85
	public function __construct(
86
		ConditionFactory $condition_factory
87
	) {
88
		$this->condition_factory = $condition_factory;
89
	}
90
91
	/**
92
	 * Register middleware.
93
	 *
94
	 * @codeCoverageIgnore
95
	 * @param  array $middleware
96
	 * @return void
97
	 */
98
	public function setMiddleware( $middleware ) {
99
		$this->middleware = $middleware;
100
	}
101
102
	/**
103
	 * Register middleware groups.
104
	 *
105
	 * @codeCoverageIgnore
106
	 * @param  array $middleware_groups
107
	 * @return void
108
	 */
109
	public function setMiddlewareGroups( $middleware_groups ) {
110
		$this->middleware_groups = $middleware_groups;
111
	}
112
113
	/**
114
	 * Register global middleware.
115
	 *
116
	 * @codeCoverageIgnore
117
	 * @param  array $middleware
118
	 * @return void
119
	 */
120
	public function setGlobalMiddleware( $middleware ) {
121
		$this->global_middleware = $middleware;
122
	}
123
124
	/**
125
	 * Register middleware execution priority.
126
	 *
127
	 * @codeCoverageIgnore
128
	 * @param  array $middleware_priority
129
	 * @return void
130
	 */
131
	public function setMiddlewarePriority( $middleware_priority ) {
132
		$this->middleware_priority = $middleware_priority;
133
	}
134
135
	/**
136
	 * Get middleware priority.
137
	 * This is in reverse compared to definition order.
138
	 * Middleware with unspecified priority will yield -1.
139
	 *
140
	 * @internal
141
	 * @param  mixed   $middleware
142
	 * @return integer
143
	 */
144 1
	public function getMiddlewarePriority( $middleware ) {
145 1
		$increasing_priority = array_reverse( $this->middleware_priority );
146 1
		$priority = array_search( $middleware, $increasing_priority );
147 1
		return $priority !== false ? (int) $priority : -1;
148
	}
149
150
	/**
151
	 * Sort array of fully qualified middleware class names by priority in ascending order.
152
	 *
153
	 * @internal
154
	 * @param  array $middleware
155
	 * @return array
156
	 */
157 1
	public function sortMiddleware( $middleware ) {
158 1
		$sorted = $middleware;
159
160 1
		usort( $sorted, function ( $a, $b ) use ( $middleware ) {
161 1
			$priority = $this->getMiddlewarePriority( $b ) - $this->getMiddlewarePriority( $a );
162
163 1
			if ( $priority !== 0 ) {
164 1
				return $priority;
165
			}
166
167
			// Keep relative order from original array.
168 1
			return array_search( $a, $middleware ) - array_search( $b, $middleware );
169 1
		} );
170
171 1
		return array_values( $sorted );
172
	}
173
174
	/**
175
	 * Expand array of middleware into an array of fully qualified class names.
176
	 *
177
	 * @internal
178
	 * @param  array<string> $middleware
179
	 * @return array<string>
180
	 */
181
	public function expandMiddleware( $middleware ) {
182
		$classes = [];
183
184
		foreach ( $middleware as $item ) {
185
			if ( isset( $this->middleware_groups[ $item ] ) ) {
186
				$classes = array_merge(
187
					$classes,
188
					$this->expandMiddlewareGroup( $item )
189
				);
190
				continue;
191
			}
192
193
			$classes[] = $this->expandMiddlewareItem( $item );
194
		}
195
196
		return $classes;
197
	}
198
199
	/**
200
	 * Expand a middleware group into an array of fully qualified class names.
201
	 *
202
	 * @internal
203
	 * @param  string        $group
204
	 * @return array<string>
205
	 */
206
	public function expandMiddlewareGroup( $group ) {
207
		if ( ! isset( $this->middleware_groups[ $group ] ) ) {
208
			throw new ConfigurationException( 'Unknown middleware group "' . $group . '" used.' );
209
		}
210
211
		return array_map( [$this, 'expandMiddlewareItem'], $this->middleware_groups[ $group ] );
212
	}
213
214
	/**
215
	 * Expand a middleware into a fully qualified class name.
216
	 *
217
	 * @internal
218
	 * @param  string $middleware
219
	 * @return string
220
	 */
221
	public function expandMiddlewareItem( $middleware ) {
222
		if ( is_subclass_of( $middleware, MiddlewareInterface::class ) ) {
223
			return $middleware;
224
		}
225
226
		if ( ! isset( $this->middleware[ $middleware ] ) ) {
227
			throw new ConfigurationException( 'Unknown middleware "' . $middleware . '" used.' );
228
		}
229
230
		return $this->middleware[ $middleware ];
231
	}
232
233
	/**
234
	 * Get the current route.
235
	 *
236
	 * @return RouteInterface
237
	 */
238 1
	public function getCurrentRoute() {
239 1
		return $this->current_route;
240
	}
241
242
	/**
243
	 * Set the current route.
244
	 *
245
	 * @param  RouteInterface
246
	 * @return void
247
	 */
248 1
	public function setCurrentRoute( RouteInterface $current_route ) {
249 1
		$this->current_route = $current_route;
250 1
	}
251
252
	/**
253
	 * Add a group to the group stack, merging all previous attributes.
254
	 *
255
	 * @param array<string, mixed> $attributes
256
	 * @return void
257
	 */
258
	protected function addGroupToStack( $attributes ) {
259
		$previous = Arr::last( $this->group_stack, null, [] );
260
261
		$condition = $this->condition_factory->merge(
262
			Arr::get( $previous, 'condition', '' ),
263
			Arr::get( $attributes, 'condition', '' )
264
		);
265
266
		$attributes = array(
267
			'condition' => $condition !== null ? $condition : '',
268
			'where' => array_merge(
269
				Arr::get( $previous, 'where', [] ),
270
				Arr::get( $attributes, 'where', [] )
271
			),
272
			'middleware' => array_merge(
273
				(array) Arr::get( $previous, 'middleware', [] ),
274
				(array) Arr::get( $attributes, 'middleware', [] )
275
			),
276
		);
277
278
		$this->group_stack[] = $attributes;
279
	}
280
281
	/**
282
	 * Remove last group from the group stack.
283
	 *
284
	 * @return void
285
	 */
286
	protected function removeLastGroupFromStack() {
287
		array_pop( $this->group_stack );
288
	}
289
290
	/**
291
	 * Create a new route group.
292
	 *
293
	 * @param array<string, mixed> $attributes
294
	 * @param \Closure            $routes
295
	 * @return void
296
	 */
297 1
	public function group( $attributes, $routes ) {
298 1
		$this->addGroupToStack( $attributes );
299
300 1
		$routes();
301
302 1
		$this->removeLastGroupFromStack();
303 1
	}
304
305
	/**
306
	 * {@inheritDoc}
307
	 */
308
	public function makeRoute( $methods, $condition, $handler ) {
309
		if ( ! $condition instanceof ConditionInterface ) {
310
			try {
311
				$condition = $this->condition_factory->make( $condition );
312
			} catch ( InvalidRouteConditionException $e ) {
313
				throw new InvalidRouteConditionException( 'Route condition is not a valid route string or condition.' );
314
			}
315
		}
316
317
		return new Route( $methods, $condition, $handler );
318
	}
319
320
	/**
321
	 * {@inheritDoc}
322
	 */
323 1
	public function addRoute( $route ) {
324 1
		$group = Arr::last( $this->group_stack, null, [] );
325 1
		$condition = $route->getCondition();
326
327 1
		if ( $condition instanceof HasUrlWhereInterface ) {
328
			$condition->setUrlWhere( array_merge(
329
				Arr::get( $group, 'where', [] ),
330
				$condition->getUrlWhere()
331
			) );
332
		}
333
334 1
		$condition = $this->condition_factory->merge(
335 1
			Arr::get( $group, 'condition', '' ),
336 1
			$condition
337
		);
338
339 1
		$route->setCondition( $condition );
340
341 1
		$route->setMiddleware( array_merge(
342 1
			Arr::get( $group, 'middleware', [] ),
343 1
			$route->getMiddleware()
344
		) );
345
346 1
		return $this->traitAddRoute( $route );
347
	}
348
349
	/**
350
	 * Handle ALL requests.
351
	 *
352
	 * @param  string|\Closure|null $handler
353
	 * @return RouteInterface
354
	 */
355 1
	public function handleAll( $handler = null ) {
356
		// Match ANY request method.
357
		// Match ANY url.
358
		// By default, use built-in WordPress controller.
359 1
		return $this->any( '*', $handler );
360
	}
361
362
	/**
363
	 * Execute a route.
364
	 *
365
	 * @param  RequestInterface  $request
366
	 * @param  RouteInterface    $route
367
	 * @param  string            $view
368
	 * @return ResponseInterface
369
	 */
370
	protected function handle( RequestInterface $request, RouteInterface $route, $view ) {
371 2
		$handler = function ( $request, $view ) use ( $route ) {
372 2
			return $route->handle( $request, $view );
373 2
		};
374
375 2
		$global_middleware = $this->expandMiddleware( $this->global_middleware );
376 2
		$route_middleware = $this->sortMiddleware( $this->expandMiddleware( $route->getMiddleware() ) );
377
378 2
		$response = ( new Pipeline() )
379 2
			->middleware( $global_middleware )
380 2
			->middleware( $route_middleware )
381 2
			->to( $handler )
382 2
			->run( $request, [$request, $view] );
383
384 2
		return $response;
385
	}
386
387
	/**
388
	 * Execute the first satisfied route, if any.
389
	 *
390
	 * @param  RequestInterface       $request
391
	 * @param  string                 $view
392
	 * @return ResponseInterface|null
393
	 */
394 2
	public function execute( $request, $view ) {
395 2
		$routes = $this->getRoutes();
396
397 2
		foreach ( $routes as $route ) {
398 2
			if ( $route->isSatisfied( $request ) ) {
399 1
				$this->setCurrentRoute( $route );
400 2
				return $this->handle( $request, $route, $view );
401
			}
402
		}
403
404 1
		return null;
405
	}
406
}
407