Router::mergeGroup()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
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 BadMethodCallException;
28
use Syscodes\Http\Response;
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
use Syscodes\Controller\MiddlewareResolver;
35
36
/**
37
 * The Router class allows the integration of an easy-to-use routing system.
38
 * 
39
 * @author Alexander Campo <[email protected]>
40
 */
41
class Router implements Routable
42
{
43
	use Concerns\RouteMap,
44
	    Concerns\RouteResolver;
45
46
	/**
47
	 * The registered route value binders.
48
	 * 
49
	 * @var array $binders
50
	 */
51
	protected $binders = [];
52
53
	/**
54
	 * The container instance used by the router.
55
	 * 
56
	 * @var \Syscodes\Contracts\Container\Container $container
57
	 */
58
	protected $container;
59
60
	/**
61
	 * Variable of group route.
62
	 *  
63
	 * @var array $groupStack
64
	 */
65
	protected $groupStack = [];
66
	
67
	/**
68
	 * The registered string macros.
69
	 * 
70
	 * @var array $macros
71
	 */
72
	protected $macros = [];
73
74
	/**
75
	 * Middleware for function of filters
76
	 *  
77
	 * @var array $middleware
78
	 */
79
	protected $middleware = [];
80
	
81
	/**
82
	 * All of the middleware groups.
83
	 * 
84
	 * @var array $middlewareGroups
85
	 */
86
	protected $middlewareGroups = [];
87
	
88
	/**
89
	 * The priority-sorted list of middleware.
90
	 * 
91
	 * @var array $middlewarePriority
92
	 */
93
	public $middlewarePriority = [];
94
	
95
	/**
96
	 * The globally available parameter patterns.
97
	 * 
98
	 * @var array $patterns
99
	 */
100
	protected $patterns = [];
101
102
	/** 
103
	 * The route collection instance. 
104
	 * 
105
	 * @var \Syscodes\Routing\RouteCollection $routes
106
	 */
107
	protected $routes;
108
109
	/**
110
	 * The Resource instance.
111
	 * 
112
	 * @var \Syscodes\Routing\ResourceRegister $resources
113
	 */
114
	protected $resources;
115
116
	/**
117
	 * Constructor. Create a new Router instance.
118
	 *
119
	 * @param  \Syscodes\Contracts\Container\Container|null  $container  (null by default)
120
	 * 
121
	 * @return void
122
	 */
123
	public function __construct(Container $container = null)
124
	{
125
		$this->routes = new RouteCollection();
126
127
		$this->container = $container ?: new Container;
128
	}
129
130
	/**
131
	 * Get the prefix from the group on the stack.
132
	 *
133
	 * @return string
134
	 */
135
	public function getGroupPrefix()
136
	{
137
		if ( ! empty($this->groupStack)) {
138
			$last = end($this->groupStack);
139
140
			return $last['prefix'] ?? '';
141
		}
142
143
		return '';
144
	}
145
146
	/**
147
	 * Group a series of routes under a single URL segment. This is handy
148
	 * for grouping items into an admin area, like:
149
	 *
150
	 *   Example:
151
	 *      // Creates route: /admin show the word 'User'
152
	 *      Route::group(['prefix' => 'admin'], function() {	 
153
	 *
154
	 *          Route::get('/user', function() {
155
	 *	            echo 'Hello world..!';
156
	 *          });
157
	 *
158
	 *      }); /admin/user
159
	 *
160
	 * @param  array  $attributes
161
	 * @param  \Closure|string  $callback
162
	 *
163
	 * @return void
164
	 */
165
	public function group(array $attributes, $callback) 
166
	{
167
		$this->updateGroupStack($attributes);
168
169
		$this->loadRoutes($callback);
170
171
		array_pop($this->groupStack);
172
173
		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...
174
	}
175
176
	/**
177
	 * Update the group stack with the given attributes.
178
	 * 
179
	 * @param  array  $attributes
180
	 * 
181
	 * @return void
182
	 */
183
	protected function updateGroupStack(array $attributes)
184
	{
185
		if ( ! empty($this->groupStack)) {
186
			$attributes = $this->mergeGroup($attributes);
187
		}
188
189
		$this->groupStack[] = $attributes;
190
	}
191
192
	/**
193
	 * Merge the given group attributes.
194
	 * 
195
	 * @param  array  $new
196
	 * 
197
	 * @return array
198
	 */
199
	protected function mergeGroup($new)
200
	{
201
		return RouteGroup::mergeGroup($new, end($this->groupStack));
202
	}
203
	
204
	/**
205
	 * Load the provided routes.
206
	 * 
207
	 * @param  \Closure|string  $callback
208
	 * 
209
	 * @return void
210
	 */
211
	protected function loadRoutes($callback)
212
	{
213
		if ($callback instanceof Closure) {
214
			$callback($this);
215
		} else {
216
			(new RouteFileRegister($this))->register($callback);
217
		}
218
	}
219
220
	/**
221
	 * Add a route to the underlying route collection. 
222
	 *
223
	 * @param  array|string  $method
224
	 * @param  string  $route
225
	 * @param  mixed  $action
226
	 *
227
	 * @return \Syscodes\Routing\Route
228
	 */
229
	public function addRoute($method, $route, $action)
230
	{
231
		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

231
		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...
232
	}
233
234
	/**
235
	 * Create a redirect from one URI to another.
236
	 * 
237
	 * @param  string  $uri
238
	 * @param  string  $destination
239
	 * @param  int  $status  (302 by default)
240
	 * 
241
	 * @return \Syscodes\Routing\Route
242
	 */
243
	public function redirect($uri, $destination, $status = 302)
244
	{
245
		return $this->any($uri, function () use ($destination, $status) {
0 ignored issues
show
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...
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...
246
			return new RedirectResponse($destination, $status);
247
		});
248
	}
249
250
	/**
251
	 * Register a new route that returns a view.
252
	 * 
253
	 * @param  string  $uri
254
	 * @param  string  $view
255
	 * @param  array  $data
256
	 * 
257
	 * @return \Syscodes\Routing\Route
258
	 */
259
	public function view($uri, $view, $data = [])
260
	{
261
		return $this->match(['GET', 'HEAD'], $uri, function () use ($view, $data) {
0 ignored issues
show
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...
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...
262
			return $this->container->make('view')->make($view, $data);
263
		});
264
	}
265
266
	/**
267
	 * Add new route to routes array.
268
	 *
269
	 * @param  array|string  $method
270
	 * @param  string  $route
271
	 * @param  mixed  $action
272
	 *
273
	 * @return void
274
	 * 
275
	 * @throws \InvalidArgumentException
276
	 */
277
	public function map($method, $route, $action) 
278
	{
279
		if ($this->actionReferencesController($action)) {
280
			$action = $this->convertToControllerAction($action);
281
		}
282
283
		$route = $this->newRoute(
284
				array_map('strtoupper', (array) $method),
285
				$this->prefix($route),
286
				$action
287
		);
288
289
		if ($this->hasGroupStack()) {
290
			$this->mergeGroupAttributesIntoRoute($route);			
291
		}
292
293
		$this->addWhereClausesToRoute($route);
294
		
295
		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...
296
	}
297
	
298
	/**
299
	 * Determine if the action is routing to a controller.
300
	 * 
301
	 * @param  array  $action
302
	 * 
303
	 * @return bool
304
	 */
305
	protected function actionReferencesController($action)
306
	{
307
		if ($action instanceof Closure) {
0 ignored issues
show
introduced by
$action is never a sub-type of Closure.
Loading history...
308
			return false;
309
		}
310
		
311
		return is_string($action) || (isset($action['uses']) && is_string($action['uses']));
312
	}
313
	
314
	/**
315
	 * Add a controller based route action to the action array.
316
	 * 
317
	 * @param  array|string  $action
318
	 * 
319
	 * @return array
320
	 */
321
	protected function convertToControllerAction($action)
322
	{
323
		if (is_string($action)) {
324
			$action = ['uses' => $action];
325
		}
326
		
327
		if ( ! empty($this->groupStack)) {
328
			$action['uses'] = $this->prependGroupUses($action['uses']);
329
		}
330
		
331
		$action['controller'] = $action['uses'];
332
		
333
		return $action;
334
	}
335
	
336
	/**
337
	 * Prepend the last group uses onto the use clause.
338
	 * 
339
	 * @param  string  $uses
340
	 * 
341
	 * @return string
342
	 */
343
	protected function prependGroupUses($uses)
344
	{
345
		$group = end($this->groupStack);
346
		
347
		return isset($group['namespace']) ? $group['namespace'].'\\'.$uses : $uses;
348
	}
349
350
	/**
351
	 * Create a new Route object.
352
	 * 
353
	 * @param  array|string  $method
354
	 * @param  string  $uri
355
	 * @param  mixed  $action
356
	 * 
357
	 * @return \Syscodes\Routing\Route
358
	 */
359
	public function newRoute($method, $uri, $action)
360
	{
361
		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

361
		return take(/** @scrutinizer ignore-type */ new Route($method, $uri, $action))
Loading history...
362
		              ->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

362
		              ->/** @scrutinizer ignore-call */ setContainer($this->container);
Loading history...
363
	}
364
	
365
	/**
366
	 * Determine if the router currently has a group stack.
367
	 * 
368
	 * @return bool
369
	 */
370
	public function hasGroupStack()
371
	{
372
		return ! empty($this->groupStack);
373
	}
374
	
375
	/**
376
	 * Merge the group stack with the controller action.
377
	 * 
378
	 * @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...
379
	 * 
380
	 * @return void
381
	 */
382
	protected function mergeGroupAttributesIntoRoute($route)
383
	{
384
		$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

384
		/** @scrutinizer ignore-call */ 
385
  $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

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