Passed
Push — 0.7.0 ( e442b6...679d5f )
by Alexander
11:21 queued 12s
created

Router::updateGroupStack()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
1
<?php 
2
3
/**
4
 * Lenevor Framework
5
 *
6
 * LICENSE
7
 *
8
 * This source file is subject to the new BSD license that is bundled
9
 * with this package in the file license.md.
10
 * It is also available through the world-wide-web at this URL:
11
 * https://lenevor.com/license
12
 * If you did not receive a copy of the license and are unable to
13
 * obtain it through the world-wide-web, please send an email
14
 * to [email protected] so we can send you a copy immediately.
15
 *
16
 * @package     Lenevor
17
 * @subpackage  Base
18
 * @link        https://lenevor.com
19
 * @copyright   Copyright (c) 2019 - 2021 Alexander Campo <[email protected]>
20
 * @license     https://opensource.org/licenses/BSD-3-Clause New BSD license or see https://lenevor.com/license or see /license.md
21
 */
22
23
namespace Syscodes\Routing;
24
25
use Closure;
26
use Syscodes\Http\Request;
27
use Syscodes\Http\Response;
28
use BadMethodCallException;
29
use InvalidArgumentException;
30
use Syscodes\Collections\Arr;
31
use Syscodes\Http\RedirectResponse;
32
use Syscodes\Contracts\Routing\Routable;
33
use Syscodes\Contracts\Container\Container;
34
35
/**
36
 * The Router class allows the integration of an easy-to-use routing system.
37
 * 
38
 * @author Alexander Campo <[email protected]>
39
 */
40
class Router implements Routable
41
{
42
	use Concerns\RouteMap,
43
	    Concerns\RouteResolver;
44
45
	/**
46
	 * The registered route value binders.
47
	 * 
48
	 * @var array $binders
49
	 */
50
	protected $binders = [];
51
52
	/**
53
	 * The container instance used by the router.
54
	 * 
55
	 * @var \Syscodes\Contracts\Container\Container $container
56
	 */
57
	protected $container;
58
59
	/**
60
	 * Variable of group route.
61
	 *  
62
	 * @var array $groupStack
63
	 */
64
	protected $groupStack = [];
65
	
66
	/**
67
	 * The registered string macros.
68
	 * 
69
	 * @var array $macros
70
	 */
71
	protected $macros = [];
72
73
	/**
74
	 * Middleware for function of filters
75
	 *  
76
	 * @var array $middleware
77
	 */
78
	protected $middleware = [];
79
	
80
	/**
81
	 * All of the middleware groups.
82
	 * 
83
	 * @var array $middlewareGroups
84
	 */
85
	protected $middlewareGroups = [];
86
	
87
	/**
88
	 * The priority-sorted list of middleware.
89
	 * 
90
	 * @var array $middlewarePriority
91
	 */
92
	public $middlewarePriority = [];
93
	
94
	/**
95
	 * The globally available parameter patterns.
96
	 * 
97
	 * @var array $patterns
98
	 */
99
	protected $patterns = [];
100
101
	/** 
102
	 * The route collection instance. 
103
	 * 
104
	 * @var \Syscodes\Routing\RouteCollection $routes
105
	 */
106
	protected $routes;
107
108
	/**
109
	 * The Resource instance.
110
	 * 
111
	 * @var \Syscodes\Routing\ResourceRegister $resources
112
	 */
113
	protected $resources;
114
115
	/**
116
	 * Constructor. Create a new Router instance.
117
	 *
118
	 * @param  \Syscodes\Contracts\Container\Container|null  $container  (null by default)
119
	 * 
120
	 * @return void
121
	 */
122
	public function __construct(Container $container = null)
123
	{
124
		$this->routes = new RouteCollection();
125
126
		$this->container = $container ?: new Container;
127
	}
128
129
	/**
130
	 * Get the prefix from the group on the stack.
131
	 *
132
	 * @return string
133
	 */
134
	public function getGroupPrefix()
135
	{
136
		if ( ! empty($this->groupStack)) {
137
			$last = end($this->groupStack);
138
139
			return $last['prefix'] ?? '';
140
		}
141
142
		return '';
143
	}
144
145
	/**
146
	 * Group a series of routes under a single URL segment. This is handy
147
	 * for grouping items into an admin area, like:
148
	 *
149
	 *   Example:
150
	 *      // Creates route: /admin show the word 'User'
151
	 *      Route::group(['prefix' => 'admin'], function() {	 
152
	 *
153
	 *          Route::get('/user', function() {
154
	 *	            echo 'Hello world..!';
155
	 *          });
156
	 *
157
	 *      }); /admin/user
158
	 *
159
	 * @param  array  $attributes
160
	 * @param  \Closure|string  $callback
161
	 *
162
	 * @return void
163
	 */
164
	public function group(array $attributes, $callback) 
165
	{
166
		$this->updateGroupStack($attributes);
167
168
		$this->loadRoutes($callback);
169
170
		array_pop($this->groupStack);
171
172
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Syscodes\Routing\Router which is incompatible with the documented return type void.
Loading history...
173
	}
174
175
	/**
176
	 * Update the group stack with the given attributes.
177
	 * 
178
	 * @param  array  $attributes
179
	 * 
180
	 * @return void
181
	 */
182
	protected function updateGroupStack(array $attributes)
183
	{
184
		if ( ! empty($this->groupStack)) {
185
			$attributes = $this->mergeGroup($attributes);
186
		}
187
188
		$this->groupStack[] = $attributes;
189
	}
190
191
	/**
192
	 * Merge the given group attributes.
193
	 * 
194
	 * @param  array  $new
195
	 * 
196
	 * @return array
197
	 */
198
	protected function mergeGroup($new)
199
	{
200
		return RouteGroup::mergeGroup($new, end($this->groupStack));
201
	}
202
	
203
	/**
204
	 * Load the provided routes.
205
	 * 
206
	 * @param  \Closure|string  $callback
207
	 * 
208
	 * @return void
209
	 */
210
	protected function loadRoutes($callback)
211
	{
212
		if ($callback instanceof Closure) {
213
			$callback($this);
214
		} else {
215
			(new RouteFileRegister($this))->register($callback);
216
		}
217
	}
218
219
	/**
220
	 * Add a route to the underlying route collection. 
221
	 *
222
	 * @param  array|string  $method
223
	 * @param  string  $route
224
	 * @param  mixed  $action
225
	 *
226
	 * @return \Syscodes\Routing\Route
227
	 */
228
	public function addRoute($method, $route, $action)
229
	{
230
		return $this->routes->add($this->map($method, $route, $action));
0 ignored issues
show
Bug introduced by
$this->map($method, $route, $action) of type void is incompatible with the type Syscodes\Routing\Route expected by parameter $route of Syscodes\Routing\RouteCollection::add(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

230
		return $this->routes->add(/** @scrutinizer ignore-type */ $this->map($method, $route, $action));
Loading history...
Bug introduced by
Are you sure the usage of $this->map($method, $route, $action) targeting Syscodes\Routing\Router::map() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
231
	}
232
233
	/**
234
	 * Create a redirect from one URI to another.
235
	 * 
236
	 * @param  string  $uri
237
	 * @param  string  $destination
238
	 * @param  int  $status  (302 by default)
239
	 * 
240
	 * @return \Syscodes\Routing\Route
241
	 */
242
	public function redirect($uri, $destination, $status = 302)
243
	{
244
		return $this->any($uri, function () use ($destination, $status) {
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->any($uri, ...ion(...) { /* ... */ }) returns the type void which is incompatible with the documented return type Syscodes\Routing\Route.
Loading history...
Bug introduced by
Are you sure the usage of $this->any($uri, function(...) { /* ... */ }) targeting Syscodes\Routing\Router::any() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
245
			return new RedirectResponse($destination, $status);
246
		});
247
	}
248
249
	/**
250
	 * Register a new route that returns a view.
251
	 * 
252
	 * @param  string  $uri
253
	 * @param  string  $view
254
	 * @param  array  $data
255
	 * 
256
	 * @return \Syscodes\Routing\Route
257
	 */
258
	public function view($uri, $view, $data = [])
259
	{
260
		return $this->match(['GET', 'HEAD'], $uri, function () use ($view, $data) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->match(array('GET'...ion(...) { /* ... */ }) targeting Syscodes\Routing\Router::match() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug Best Practice introduced by
The expression return $this->match(arra...ion(...) { /* ... */ }) returns the type void which is incompatible with the documented return type Syscodes\Routing\Route.
Loading history...
261
			return $this->container->make('view')->make($view, $data);
262
		});
263
	}
264
265
	/**
266
	 * Add new route to routes array.
267
	 *
268
	 * @param  array|string  $method
269
	 * @param  string  $route
270
	 * @param  mixed  $action
271
	 *
272
	 * @return void
273
	 * 
274
	 * @throws \InvalidArgumentException
275
	 */
276
	public function map($method, $route, $action) 
277
	{
278
		if ($this->actionReferencesController($action)) {
279
			$action = $this->convertToControllerAction($action);
280
		}
281
282
		$route = $this->newRoute(
283
				array_map('strtoupper', (array) $method),
284
				$this->prefix($route),
285
				$action
286
		);
287
288
		if ($this->hasGroupStack()) {
289
			$this->mergeGroupAttributesIntoRoute($route);			
290
		}
291
292
		$this->addWhereClausesToRoute($route);
293
		
294
		return $route;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $route returns the type Syscodes\Routing\Route which is incompatible with the documented return type void.
Loading history...
295
	}
296
	
297
	/**
298
	 * Determine if the action is routing to a controller.
299
	 * 
300
	 * @param  array  $action
301
	 * 
302
	 * @return bool
303
	 */
304
	protected function actionReferencesController($action)
305
	{
306
		if ($action instanceof Closure) {
0 ignored issues
show
introduced by
$action is never a sub-type of Closure.
Loading history...
307
			return false;
308
		}
309
		
310
		return is_string($action) || (isset($action['uses']) && is_string($action['uses']));
311
	}
312
	
313
	/**
314
	 * Add a controller based route action to the action array.
315
	 * 
316
	 * @param  array|string  $action
317
	 * 
318
	 * @return array
319
	 */
320
	protected function convertToControllerAction($action)
321
	{
322
		if (is_string($action)) {
323
			$action = ['uses' => $action];
324
		}
325
		
326
		if ( ! empty($this->groupStack)) {
327
			$action['uses'] = $this->prependGroupUses($action['uses']);
328
		}
329
		
330
		$action['controller'] = $action['uses'];
331
		
332
		return $action;
333
	}
334
	
335
	/**
336
	 * Prepend the last group uses onto the use clause.
337
	 * 
338
	 * @param  string  $uses
339
	 * 
340
	 * @return string
341
	 */
342
	protected function prependGroupUses($uses)
343
	{
344
		$group = end($this->groupStack);
345
		
346
		return isset($group['namespace']) ? $group['namespace'].'\\'.$uses : $uses;
347
	}
348
349
	/**
350
	 * Create a new Route object.
351
	 * 
352
	 * @param  array|string  $method
353
	 * @param  string  $uri
354
	 * @param  mixed  $action
355
	 * 
356
	 * @return \Syscodes\Routing\Route
357
	 */
358
	public function newRoute($method, $uri, $action)
359
	{
360
		return take(new Route($method, $uri, $action))
0 ignored issues
show
Bug introduced by
new Syscodes\Routing\Route($method, $uri, $action) of type Syscodes\Routing\Route is incompatible with the type string expected by parameter $value of take(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

360
		return take(/** @scrutinizer ignore-type */ new Route($method, $uri, $action))
Loading history...
361
		              ->setContainer($this->container);
0 ignored issues
show
Bug introduced by
The method setContainer() does not exist on Syscodes\Collections\HigherOrderTakeProxy. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

361
		              ->/** @scrutinizer ignore-call */ setContainer($this->container);
Loading history...
362
	}
363
	
364
	/**
365
	 * Determine if the router currently has a group stack.
366
	 * 
367
	 * @return bool
368
	 */
369
	public function hasGroupStack()
370
	{
371
		return ! empty($this->groupStack);
372
	}
373
	
374
	/**
375
	 * Merge the group stack with the controller action.
376
	 * 
377
	 * @param  \Syscpde\Routing\Route  $route
0 ignored issues
show
Bug introduced by
The type Syscpde\Routing\Route was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
378
	 * 
379
	 * @return void
380
	 */
381
	protected function mergeGroupAttributesIntoRoute($route)
382
	{
383
		$action = static::mergeGroup($route->getAction(), end($this->groupStack));
0 ignored issues
show
Unused Code introduced by
The call to Syscodes\Routing\Router::mergeGroup() has too many arguments starting with end($this->groupStack). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

383
		/** @scrutinizer ignore-call */ 
384
  $action = static::mergeGroup($route->getAction(), end($this->groupStack));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug Best Practice introduced by
The method Syscodes\Routing\Router::mergeGroup() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

383
		/** @scrutinizer ignore-call */ 
384
  $action = static::mergeGroup($route->getAction(), end($this->groupStack));
Loading history...
384
		
385
		$route->setAction($action);
386
	}
387
	
388
	/**
389
	 * Add the necessary where clauses to the route based on its initial registration.
390
	 * 
391
	 * @param  \Syscodes\Routing\Route  $route
392
	 * 
393
	 * @return \Syscodes\Routing\Route
394
	 */
395
	protected function addWhereClausesToRoute($route)
396
	{
397
		return $route->where(array_merge(
398
			$this->patterns, Arr::get($route->getAction(), 'where', [])
399
		));
400
	}
401
402
	/**
403
	 * Add a prefix to the route URI.
404
	 *
405
	 * @param  string  $uri
406
	 *
407
	 * @return string
408
	 */
409
	protected function prefix($uri)
410
	{
411
		$uri = is_null($uri) ? '' : trim($uri, '/').'/';
0 ignored issues
show
introduced by
The condition is_null($uri) is always false.
Loading history...
412
413
		$uri = filter_var($uri, FILTER_SANITIZE_STRING);
414
415
		// While we want to add a route within a group of '/',
416
		// it doens't work with matching, so remove them...
417
		if ($uri != '/') {
418
			$uri = ltrim($uri, '/');
419
		}
420
421
		return trim(trim($this->getGroupPrefix(), '/').'/'.trim($uri, '/'), '/') ?: '/';
422
	}
423
424
	/**
425
	 * Set a global where pattern on all routes.
426
	 * 
427
	 * @param  string  $name
428
	 * @param  string  $pattern
429
	 * 
430
	 * @return void
431
	 */
432
	public function pattern($name, $pattern)
433
	{
434
		return $this->patterns[$name] = $pattern;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->patterns[$name] = $pattern returns the type string which is incompatible with the documented return type void.
Loading history...
435
	}
436
437
	/**
438
	 * Set a group of global where patterns on all routes.
439
	 * 
440
	 * @param  array  $patterns
441
	 * 
442
	 * @return void
443
	 */
444
	public function patterns($patterns)
445
	{
446
		foreach ($patterns as $key => $pattern) {
447
			$this->patterns[$key] = $pattern;
448
		}
449
	}
450
451
	/**
452
	 * Get a Resource instance.
453
	 * 
454
	 * @return \Syscodes\Routing\ResourceRegister
455
	 */
456
	public function getResource()
457
	{
458
		if (isset($this->resources)) {
459
			return $this->resources;
460
		}
461
462
		return $this->resources = new ResourceRegister($this);
463
	}
464
465
	/**
466
	 * Dispatches the given url and call the method that belongs to the route.
467
	 *
468
	 * @param  \Syscodes\Http\Request  $request
469
	 *
470
	 * @return mixed
471
	 */
472
	public function dispatch(Request $request)
473
	{
474
		return $this->resolve($request);
475
	}
476
477
	/**
478
	 * Gather the middleware for the given route.
479
	 * 
480
	 * @param  \Syscodes\Routing\Route  $route
481
	 * 
482
	 * @return array
483
	 */
484
	public function gatherRouteMiddleware(Route $route)
485
	{
486
		$middleware = array_map(function ($name) {
487
            return MiddlewareResolver::resolve($name, $this->middleware, $this->middlewareGroups);
488
        }, $route->gatherMiddleware());
489
490
        return Arr::flatten($middleware);
491
	}
492
493
	/**
494
	 * Get all of the defined middleware
495
	 * 
496
	 * @return array
497
	 */
498
	public function getMiddleware()
499
	{
500
		return $this->middleware;
501
	}
502
503
	/**
504
	 * Register a short-hand name for a middleware.
505
	 * 
506
	 * @param  string  $name
507
	 * @param  string  $class
508
	 * 
509
	 * @return $this
510
	 */
511
	public function aliasMiddleware($name, $class)
512
	{
513
		$this->middleware[$name] = $class;
514
515
		return $this;
516
	}
517
518
	/**
519
	 * Register a group of middleware.
520
	 * 
521
	 * @param  string  $name
522
	 * @param  array  $middleware
523
	 * 
524
	 * @return $this
525
	 */
526
	public function middlewareGroup($name, array $middleware)
527
	{
528
		$this->middlewareGroups[$name] = $middleware;
529
530
		return $this;
531
	}
532
533
	/**
534
	 * Check if a route with the given name exists.
535
	 * 
536
	 * @param  string  $name
537
	 * 
538
	 * @return bool
539
	 */
540
	public function has($name)
541
	{
542
		$names = is_array($name) ? $name : func_get_args();
543
544
		foreach ($names as $value) {
545
			if ( ! $this->routes->hasNamedRoute($value)) {
546
				return false;
547
			}
548
		}
549
550
		return true;
551
	}
552
553
	/**
554
	 * Get the currently dispatched route instance.
555
	 * 
556
	 * @return \Syscodes\Routing\Route|null
557
	 */
558
	public function current()
559
	{
560
		return $this->current;
561
	}
562
563
	/**
564
	 * Determine if the current route matches a pattern.
565
	 * 
566
	 * @param  mixed  ...$patterns
567
	 * 
568
	 * @return bool
569
	 */
570
	public function is(...$patterns)
571
	{
572
		return $this->currentRouteNamed(...$patterns);
573
	}
574
575
	/**
576
	 * Determine if the current route matches a pattern.
577
	 * 
578
	 * @param  mixed  ...$patterns
579
	 * 
580
	 * @return bool
581
	 */
582
	public function currentRouteNamed(...$patterns)
583
	{
584
		return $this->current() && $this->current()->named(...$patterns);
585
	}
586
587
	/**
588
	 * Register an array of resource controllers.
589
	 * 
590
	 * @param  array  $resources
591
	 * @param  array  $options
592
	 * 
593
	 * @return void
594
	 */
595
	public function resources(array $resources, array $options = [])
596
	{
597
		foreach ($resources as $name => $controller) {
598
			$this->resource($name, $controller, $options);
599
		}
600
	}
601
602
	/**
603
	 * Route a resource to a controller.
604
	 * 
605
	 * @param  string  $name
606
	 * @param  string  $controller
607
	 * @param  array  $options
608
	 * 
609
	 * @return \Syscodes\Routing\AwaitingResourceRegistration
610
	 */
611
	public function resource($name, $controller, array $options = []) 
612
	{
613
		if ($this->container) {
614
			$register = $this->container->make(ResourceRegister::class);
615
		} else {
616
			$register = new ResourceRegister($this);
617
		}
618
619
		return new AwaitingResourceRegistration(
620
			$register, $name, $controller, $options
621
		);
622
	}
623
624
	/**
625
	 * Get the route collection.
626
	 *
627
	 * @return array   
628
	 */
629
	public function getRoutes()
630
	{
631
		return $this->routes;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->routes returns the type Syscodes\Routing\RouteCollection which is incompatible with the documented return type array.
Loading history...
632
	}
633
634
	/**
635
	 * Get or set the verbs used in the resource URIs.
636
	 * 
637
	 * @param  array  $verbs
638
	 * 
639
	 * @return array|null
640
	 */
641
	public function resourceVerbs(array $verbs = [])
642
	{
643
		ResourceRegister::verbs($verbs);
644
	}
645
	
646
	/**
647
	 * Register a custom macro.
648
	 * 
649
	 * @param  string  $name
650
	 * @param  callable  $callback
651
	 * 
652
	 * @return void
653
	 */
654
	public function macro($name, callable $callback)
655
	{
656
		$this->macros[$name] = $callback;
657
	}
658
	
659
	/**
660
	 * Checks if macro is registered.
661
	 * 
662
	 * @param  string  $name
663
	 * 
664
	 * @return boolean
665
	 */
666
	public function hasMacro($name)
667
	{
668
		return isset($this->macros[$name]);
669
	}
670
	
671
	/**
672
	 * Dynamically handle calls into the router instance.
673
	 * 
674
	 * @param  string  $method
675
	 * @param  array  $parameters
676
	 * 
677
	 * @return mixed
678
	 */
679
	public function __call($method, $parameters)
680
	{
681
		if (isset($this->macros[$method])) {
682
			$callback = $this->macros[$method];
683
684
			return call_user_func_array($callback, $parameters);
685
		}
686
		
687
		return (new RouteRegister($this))->attribute($method, $parameters[0]);
688
	}
689
}