Completed
Push — master ( bfcd30...39dce4 )
by Anderson
49:49
created

Route::group()   D

Complexity

Conditions 14
Paths 256

Size

Total Lines 58
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 14
eloc 31
c 4
b 0
f 0
nc 256
nop 2
dl 0
loc 58
rs 4.7549

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Route class
5
 *
6
 * @author    Anderson Salas <[email protected]>
7
 * @copyright 2017
8
 * @license   GNU-3.0
9
 *
10
 */
11
12
namespace Luthier\Core;
13
14
class Route
15
{
16
17
    /**
18
     * Supported HTTP Verbs for this class
19
     *
20
     * @var static array $http_verbs Array of supported HTTP Verbs
21
     *
22
     * @access protected
23
     */
24
    protected static $http_verbs = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'TRACE', 'CONNECT', 'HEAD'];
25
26
27
    /**
28
     * All improved routes parsed
29
     *
30
     * @var static array $routes Array of routes with meta data
31
     *
32
     * @access protected
33
     */
34
    protected static $routes = array();
35
36
37
    /**
38
     * CodeIgniter 'default_controller' index of the $route variable in config/routes.php
39
     *
40
     * @var static $defaultController
41
     *
42
     * @access protected
43
     */
44
    protected static $defaultController;
45
46
47
    /**
48
     * CodeIgniter '404_override' index of the $route variable in config/routes.php
49
     *
50
     * @var static $_404page
51
     *
52
     * @access protected
53
     */
54
    protected static $_404page = NULL;
55
56
57
    /**
58
     * CodeIgniter 'translate_uri_dashes' index of the $route variable in config/routes.php
59
     *
60
     * @var static $translateDashes
61
     *
62
     * @access protected
63
     */
64
    protected static $translateDashes = FALSE;
65
66
67
    /**
68
     * Array of hidden routes, it will parsed as an route with a show_404() callback into a clousure
69
     *
70
     * @var static $hiddenRoutes
71
     *
72
     * @access protected
73
     */
74
    protected static $hiddenRoutes = array();
75
76
77
    /**
78
     * Route group '$hideOriginal' directive
79
     *
80
     * @var static $hideOriginals
81
     *
82
     * @access protected
83
     */
84
    protected static $hideOriginals = [];
85
86
87
    /**
88
     * Route group prefix
89
     *
90
     * @var static $prefix
91
     *
92
     * @access protected
93
     */
94
    protected static $prefix = [];
95
96
97
    /**
98
     * Route group namespace
99
     *
100
     * @var static $namespace
101
     *
102
     * @access protected
103
     */
104
    protected static $namespace = [];
105
106
107
    /**
108
     * Route group middleware
109
     *
110
     * @var static $middleware [add description]
111
     *
112
     * @access protected
113
     */
114
    protected static $middleware = [];
115
116
117
    /**
118
     * Generic method to add a improved route
119
     *
120
     * @param  mixed $verb String or array of string of valid HTTP Verbs that will be accepted in this route
121
     * @param  array $attr Associative array of route attributes
122
     * @param  bool $hideOriginal (Optional) map the original $url as a route with a show_404() callback inside
123
     *
124
     * @return void
125
     *
126
     * @access public
127
     * @static
128
     */
129
    public static function add($verb, $path, $attr, $hideOriginal = TRUE, $return = FALSE)
130
    {
131
        if (!is_array($attr))
132
        {
133
            show_error('You must specify the route attributes as an array', 500, 'Route error: bad attributes');
134
        }
135
136
        if (!isset($attr['uses']))
137
        {
138
            show_error('Route requires a \'controller@method\' to be pointed and it\'s not defined in the route attributes', 500, 'Route error: missing controller');
139
        }
140
141
        if (!preg_match('/^([a-zA-Z1-9-_]+)@([a-zA-Z1-9-_]+)$/', $attr['uses'], $parsedController) !== FALSE)
142
        {
143
            show_error('Route controller must be in format controller@method', 500, 'Route error: bad controller format');
144
        }
145
146
        $controller = $parsedController[1];
147
        $method     = $parsedController[2];
148
149
        if (!is_string($path))
150
            show_error('Route path must be a string ', 500, 'Route error: bad route path');
151
152
        if (!is_string($verb))
153
            show_error('Route HTTP Verb must be a string', 500, 'Route error: bad verb type');
154
155
        $verb = strtoupper($verb);
156
157
        if (!in_array($verb, self::$http_verbs, TRUE))
158
        {
159
            $errorMsg = 'Verb "'.$verb.'" is not a valid HTTP Verb, allowed verbs:<ul>';
160
            foreach (self::$http_verbs as $validVerb)
0 ignored issues
show
Bug introduced by
The expression self::$http_verbs of type object<Luthier\Core\Route> is not traversable.
Loading history...
161
            {
162
                $errorMsg .= '<li>'.$validVerb.'</li>';
163
            }
164
            $errorMsg .= '</ul>';
165
            show_error($errorMsg, 500, 'Route error: unknow method');
166
        }
167
168
        $route['verb'] = $verb;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$route was never initialized. Although not strictly required by PHP, it is generally a good practice to add $route = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
169
170
        $path = trim($path, '/');
171
172
        $route['path']       = $path;
173
        $route['controller'] = $controller;
174
        $route['method']     = $method;
175
176
        if (isset($attr['as']))
177
        {
178
            $route['name'] = $attr['as'];
179
        } else
180
        {
181
            $route['name'] = NULL;
182
        }
183
184
        // Setting up the prefix
185
186
        $route['prefix'] = NULL;
187
        $group_prefix = implode('/', self::$prefix);
188
189
        if ($group_prefix)
190
            $route['prefix'] = $group_prefix.'/';
191
192
        if (isset($attr['prefix']))
193
            $route['prefix'] .= $attr['prefix'];
194
195
        // Setting up the namespace
196
197
        $route['namespace'] = NULL;
198
        $group_namespace = implode('/', self::$namespace);
199
200
        if (!is_null($group_namespace))
201
            $route['namespace'] = $group_namespace.'/';
202
        if (isset($attr['namespace']))
203
            $route['namespace'] .= $attr['namespace'];
204
205
        $route['prefix']    = trim($route['prefix'], '/');
206
        $route['namespace'] = trim($route['namespace'], '/');
207
208
        if (empty($route['prefix']))
209
            $route['prefix'] = NULL;
210
211
        if (empty($route['namespace']))
212
            $route['namespace'] = NULL;
213
214
        // Route middleware
215
        $route['middleware'] = [];
216
        $route['middleware'] = array_merge($route['middleware'], self::$middleware);
217
218
        if (isset($attr['middleware']))
219
        {
220
            if (is_array($attr['middleware']))
221
            {
222
                foreach ($attr['middleware'] as $middleware)
223
                    $route['middleware'][] = $middleware; # Group
224
            }
225
            elseif (is_string($attr['middleware']))
226
            {
227
                $route['middleware'][] = $attr['middleware']; # Group
228
            }
229
            else
230
            {
231
                show_error('Route middleware must be a string or an array', 500, 'Route error: bad middleware format');
232
            }
233
        }
234
235
        $compiledRoute = self::compileRoute((object) $route);
236
237
        $route['compiled'] =
238
            [
239
                $compiledRoute->path => $compiledRoute->route
240
            ];
241
242
        $groupHideOriginals = end(self::$hideOriginals);
243
244
        if ($hideOriginal || $groupHideOriginals || ($compiledRoute->path != '' && $compiledRoute->path != '/'))
245
        {
246
            $hiddenRoutePath      = $controller.'/'.$method;
247
            $hiddenRouteNamespace = '';
248
249
            if (!is_null($route['namespace']))
250
            {
251
                $hiddenRouteNamespace = $route['namespace'].'/';
252
            }
253
254
            $hiddenRoutePath = $hiddenRouteNamespace.$hiddenRoutePath;
255
256
            if ($method == 'index')
257
            {
258
                self::$hiddenRoutes[] = [$hiddenRouteNamespace.$controller  => function() { self::trigger404(); }];
259
            }
260
261
            self::$hiddenRoutes[] = [$hiddenRoutePath => function() { self::trigger404(); }];
262
        }
263
264
        if (!$return)
265
        {
266
            self::$routes[] = (object) $route;
267
        } else
268
        {
269
            return (object) $route;
270
        }
271
    }
272
273
274
    /**
275
     * Adds a GET route, alias of Route::add('GET',$url,$attr,$hideOriginal)
276
     *
277
     * @param  string $url String or array of strings that will trigger this route
278
     * @param  array $attr Associative array of route attributes
279
     * @param  bool $hideOriginal (Optional) map the original $url as a route with a show_404() callback inside
280
     *
281
     * @return void
282
     *
283
     * @access public
284
     * @static
285
     */
286
    public static function get($url, $attr, $hideOriginal = TRUE)
287
    {
288
        self::add('GET', $url, $attr, $hideOriginal);
289
    }
290
291
292
    /**
293
     * Adds a POST route, alias of Route::add('POST',$url,$attr,$hideOriginal)
294
     *
295
     * @param  string $url String or array of strings that will trigger this route
296
     * @param  array $attr Associative array of route attributes
297
     * @param  bool $hideOriginal (Optional) map the original $url as a route with a show_404() callback inside
298
     *
299
     * @return void
300
     *
301
     * @access public
302
     * @static
303
     */
304
    public static function post($url, $attr, $hideOriginal = TRUE)
305
    {
306
        self::add('POST', $url, $attr, $hideOriginal);
307
    }
308
309
310
    /**
311
     * Adds a PUT route, alias of Route::add('PUT',$url,$attr,$hideOriginal)
312
     *
313
     * @param  mixed $url String or array of strings that will trigger this route
314
     * @param  array $attr Associative array of route attributes
315
     * @param  bool $hideOriginal (Optional) map the original $url as a route with a show_404() callback inside
316
     *
317
     * @return void
318
     *
319
     * @access public
320
     * @static
321
     */
322
    public static function put($url, $attr, $hideOriginal = TRUE)
323
    {
324
        self::add('PUT', $url, $attr, $hideOriginal);
325
    }
326
327
328
    /**
329
     * Adds a PATCH route, alias of Route::add('PATCH',$url,$attr,$hideOriginal)
330
     *
331
     * @param  mixed $url String or array of strings that will trigger this route
332
     * @param  array $attr Associative array of route attributes
333
     * @param  bool $hideOriginal (Optional) map the original $url as a route with a show_404() callback inside
334
     *
335
     * @return void
336
     *
337
     * @access public
338
     * @static
339
     */
340
    public static function patch($url, $attr, $hideOriginal = TRUE)
341
    {
342
        self::add('PATCH', $url, $attr, $hideOriginal);
343
    }
344
345
346
    /**
347
     * Adds a DELETE route, alias of Route::add('DELETE',$url,$attr,$hideOriginal)
348
     *
349
     * @param  string $url String or array of strings that will trigger this route
350
     * @param  array $attr Associative array of route attributes
351
     * @param  bool $hideOriginal (Optional) map the original $url as a route with a show_404() callback inside
352
     *
353
     * @return void
354
     *
355
     * @access public
356
     * @static
357
     */
358
    public static function delete($url, $attr, $hideOriginal = TRUE)
359
    {
360
        self::add('DELETE', $url, $attr, $hideOriginal);
361
    }
362
363
364
    /**
365
     * Adds a route with ALL accepted verbs on Route::$http_verbs
366
     *
367
     * @param  mixed $url String or array of strings that will trigger this route
368
     * @param  array $attr Associative array of route attributes
369
     * @param  bool $hideOriginal (Optional) map the original $url as a route with a show_404() callback inside
370
     *
371
     * @return void
372
     *
373
     * @access public
374
     * @static
375
     */
376
    public static function any($url, $attr, $hideOriginal = TRUE)
377
    {
378
        foreach (self::$http_verbs as $verb)
0 ignored issues
show
Bug introduced by
The expression self::$http_verbs of type object<Luthier\Core\Route> is not traversable.
Loading history...
379
        {
380
            $verb = strtolower($verb);
381
            self::add($verb, $url, $attr, $hideOriginal);
382
        }
383
    }
384
385
386
    /**
387
     * Adds a list of routes with the verbs contained in $verbs, alias of Route::add($verbs,$url,$attr,$hideOriginal)
388
     *
389
     * @param  string[] $verbs String or array of string of valid HTTP Verbs that will be accepted in this route
390
     * @param  string $url String or array of strings that will trigger this route
391
     * @param  array $attr Associative array of route attributes
392
     * @param  bool $hideOriginal (Optional) map the original $url as a route with a show_404() callback inside
393
     *
394
     * @return void
395
     *
396
     * @access public
397
     * @static
398
     */
399
    public static function matches($verbs, $url, $attr, $hideOriginal = FALSE)
400
    {
401
        if (!is_array($verbs))
402
            show_error('Route::matches() first argument must be an array of valid HTTP Verbs', 500, 'Route error: bad Route::matches() verb list');
403
404
        foreach ($verbs as $verb)
405
        {
406
            self::add($verb, $url, $attr, $hideOriginal);
407
        }
408
    }
409
410
411
    /**
412
     * Adds a RESTFul route wich contains methods for create, read, update, view an specific resource
413
     *
414
     *
415
     * @param  string $name
416
     * @param  string $controller
417
     * @param  array $attr
418
     *
419
     * @return void
420
     *
421
     * @access public
422
     * @static
423
     */
424
    public static function resource($name, $controller, $attr = NULL)
425
    {
426
        $base_attr = [];
427
428
        $hideOriginal = FALSE;
429
430
        if (isset($attr['namespace']))
431
            $base_attr['namespace']  = $attr['namespace'];
432
433
        if (isset($attr['middleware']))
434
            $base_attr['middleware'] = $attr['middleware'];
435
436
        if (isset($attr['hideOriginal']))
437
            $hideOriginal = (bool) $attr['hideOriginal'];
438
439
        $base_attr['prefix'] = strtolower($name);
440
441
        if (isset($attr['prefix']))
442
            $base_attr['prefix'] = $attr['prefix'];
443
444
        $only = [];
445
446
        $controller = strtolower($controller);
447
448
        if (isset($attr['only']) && (is_array($attr['only']) || is_string($attr['only'])))
449
        {
450
            if (is_array($attr['only']))
451
            {
452
                $only = $attr['only'];
453
            }
454
            else
455
            {
456
                $only[] = $attr['only'];
457
            }
458
        }
459
460 View Code Duplication
        if (empty($only) || in_array('index', $only))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
461
        {
462
            $route_attr = array_merge($base_attr, ['uses' => $controller.'@index', 'as' => $name.'.index']);
463
            self::get('/', $route_attr, $hideOriginal);
464
        }
465
466 View Code Duplication
        if (empty($only) || in_array('create', $only))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
467
        {
468
            $route_attr = array_merge($base_attr, ['uses' => $controller.'@create', 'as' => $name.'.create']);
469
            self::get('create', $route_attr, $hideOriginal);
470
        }
471
472 View Code Duplication
        if (empty($only) || in_array('store', $only))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
473
        {
474
            $route_attr = array_merge($base_attr, ['uses' => $controller.'@store', 'as' => $name.'.store']);
475
            self::post('/', $route_attr, $hideOriginal);
476
        }
477
478 View Code Duplication
        if (empty($only) || in_array('show', $only))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
479
        {
480
            $route_attr = array_merge($base_attr, ['uses' => $controller.'@show', 'as' => $name.'.show']);
481
            self::get('{slug}', $route_attr, $hideOriginal);
482
        }
483
484 View Code Duplication
        if (empty($only) || in_array('edit', $only))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
485
        {
486
            $route_attr = array_merge($base_attr, ['uses' => $controller.'@edit', 'as' => $name.'.edit']);
487
            self::get('{slug}/edit', $route_attr, $hideOriginal);
488
        }
489
490 View Code Duplication
        if (empty($only) || in_array('update', $only))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
491
        {
492
            $route_attr = array_merge($base_attr, ['uses' => $controller.'@update', 'as' => $name.'.update']);
493
            self::matches(['PUT', 'PATCH'], '{slug}/update', $route_attr, $hideOriginal);
494
        }
495
496 View Code Duplication
        if (empty($only) || in_array('destroy', $only))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
497
        {
498
            $route_attr = array_merge($base_attr, ['uses' => $controller.'@destroy', 'as' => $name.'.destroy']);
499
            self::delete('{slug}', $route_attr, $hideOriginal);
500
        }
501
    }
502
503
504
    /**
505
     * Compiles an improved route to a valid CodeIgniter route
506
     *
507
     * @param  array $route an improved route
508
     *
509
     * @return array
510
     *
511
     * @access public
512
     * @static
513
     */
514
    public static function compileRoute($route)
515
    {
516
        $prefix    = NULL;
517
        $namespace = NULL;
518
519
        if (!is_null($route->prefix))
520
        {
521
            $prefix = $route->prefix;
522
        }
523
524
        if (!is_null($route->namespace))
525
        {
526
            $namespace = $route->namespace;
527
        }
528
529
        $path = $route->path;
530
531
        if (!is_null($prefix))
532
            $path = $prefix.'/'.$path;
533
534
        $controller = $route->controller.'/'.$route->method;
535
536
        if (!is_null($namespace))
537
            $controller = $namespace.'/'.$controller;
538
539
        $path       = trim($path, '/');
540
        $controller = trim($controller, '/');
541
        $baseController = $controller;
542
543
        $replaces =
544
            [
545
                '{\((.*)\):[a-zA-Z0-9-_]*(\?}|})' => '($1)', # Custom regular expression
546
                '{num:[a-zA-Z0-9-_]*(\?}|})'      => '(:num)', # (:num) route
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
547
                '{any:[a-zA-Z0-9-_]*(\?}|})'      => '(:any)', # (:any) route
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
548
                '{[a-zA-Z0-9-_]*(\?}|})'          => '(:any)', # Everything else
549
            ];
550
551
        $foundedArgs = [];
552
        $basePath    = '';
553
554
555
        foreach (explode('/', $path) as $path_segment)
556
        {
557
            if (!preg_match('/^\{(.*)\}$/', $path_segment))
558
            {
559
                $basePath .= $path_segment.'/';
560
            }
561
        }
562
563
        $basePath = trim($basePath, '/');
564
565
        $segments = explode('/', $path);
566
567
        foreach ($segments as $key => &$segment)
568
        {
569
            $customRegex = FALSE;
570
571
            foreach ($replaces as $regex => $replace)
572
            {
573
                if($customRegex)
574
                    continue;
575
576
                $matches = [];
0 ignored issues
show
Unused Code introduced by
$matches is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
577
578
                if(preg_match('/^\{(.*)\}$/', $segment))
579
                {
580
                    $foundedArgs[$key] = $segment;
581
                }
582
583
                $c = 0;
584
                $segment = preg_replace('/'.$regex.'/', $replace, $segment, 1, $c);
585
586
                if( $regex == array_keys($replaces)[0] && $c > 0)
587
                    $customRegex = TRUE;
588
            }
589
        }
590
591
        $path = implode('/', $segments);
592
593
        $argConstraint = FALSE;
594
595
        $args = [];
596
        $args['required'] = [];
597
        $args['optional'] = [];
598
599
        foreach ($foundedArgs as $arg)
600
        {
601
            if (substr($arg, -2) == '?}')
602
            {
603
                $args['optional'][] = $arg;
604
                $argConstraint = TRUE;
605
            }
606
            else
607
            {
608
                if ($argConstraint)
609
                    show_error('Optional route path argument not valid at this position', 500, 'Route error');
610
                $args['required'][] = $arg;
611
            }
612
        }
613
614
        $c_foundedArgs = count($foundedArgs);
615
616
        if ($c_foundedArgs > 0)
617
        {
618
            for ($i = 0; $i < $c_foundedArgs; $i++)
619
            {
620
                $controller .= '/$'.($i + 1);
621
            }
622
        }
623
624
        return (object) [
625
            'path'      => $path,
626
            'route'     => $controller,
627
            'args'      => $args,
628
            'baseRoute' => $baseController,
629
            'basePath'  => $basePath
630
        ];
631
    }
632
633
634
    /**
635
     * Compile ALL improved routes into a valid CodeIgniter's associative array of routes
636
     *
637
     * @return array
638
     *
639
     * @access public
640
     * @static
641
     */
642
    public static function register()
643
    {
644
        $routes = array();
645
646
        foreach (self::$routes as $index => $route)
0 ignored issues
show
Bug introduced by
The expression self::$routes of type object<Luthier\Core\Route> is not traversable.
Loading history...
647
        {
648
            $compiled = self::compileRoute($route);
649
650
            $backtrackingPath  = '';
0 ignored issues
show
Unused Code introduced by
$backtrackingPath is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
651
            $backtrackingRoute = '';
0 ignored issues
show
Unused Code introduced by
$backtrackingRoute is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
652
653
            $c_reqArgs = count($compiled->args['required']);
654
            $c_optArgs = count($compiled->args['optional']);
655
656
            if ($c_optArgs > 0)
657
            {
658
                $e_path  = explode('/', $compiled->path);
659
                $e_route = explode('/', $compiled->route);
0 ignored issues
show
Unused Code introduced by
$e_route is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
660
661
                $basePath = $compiled->basePath;
662
                $baseRoute = $compiled->baseRoute;
663
664
                $a = count(explode('/', $basePath));
665
666
                for ($r = 0; $r < $c_reqArgs; $r++)
667
                {
668
                    $basePath .= '/'.$e_path[$a + $r];
669
                    $baseRoute .= '/'.'$'.($r + 1);
670
                }
671
672
                $a = count(explode('/', $basePath));
673
                $b = ($r + 1);
674
675
                $backtracking = [];
676
677
                for ($o = 0; $o <= $c_optArgs; $o++)
678
                {
679
                    $backtrackingPath  = $basePath;
680
                    $backtrackingRoute = $baseRoute;
681
682
                    for ($c = 0; $c < $o; $c++)
683
                    {
684
                        $backtrackingPath .= '/'.$e_path[$a + $c - 1];
685
                        $backtrackingRoute .= '/'.'$'.($b + $c);
686
                    }
687
688
                    $backtracking[$o] = ['path' => $backtrackingPath, 'route' => $backtrackingRoute];
689
                }
690
691
                foreach ($backtracking as $b_route)
692
                {
693
                    $b_compiled   = self::compileRoute($route);
694
                    $b_args       = array_merge($b_compiled->args['required'], $b_compiled->args['optional']);
695
                    $b_route_path = $b_route['path'];
696
697
                    foreach ($b_args as $arg)
698
                    {
699
                        $b_route_path = preg_replace('/\((.*?)\)/', $arg, $b_route_path, 1);
700
                    }
701
702
                    self::add($route->verb, $b_route_path, ['uses' => $route->controller.'@'.$route->method]);
703
704
                    if (!isset($routes[$b_route['path']]) || $route->verb == 'GET')
705
                    {
706
                        $routes[$b_route['path']] = $b_route['route'];
707
                    }
708
                }
709
            }
710
711
            if (!isset($routes[$compiled->path]) || $route->verb == 'GET')
712
            {
713
                $routes[$compiled->path] = $compiled->route;
714
            }
715
716
            self::$routes[$index] = (object) $route;
717
        }
718
719
        foreach (self::$hiddenRoutes as $route)
0 ignored issues
show
Bug introduced by
The expression self::$hiddenRoutes of type object<Luthier\Core\Route> is not traversable.
Loading history...
720
        {
721
            $path = key($route);
722
            $_404 = $route[$path];
723
724
            if (!isset($routes[$path]))
725
                $routes[$path] = $_404;
726
        }
727
728
        if (is_null(self::$defaultController))
729
            show_error('You must specify a home route: Route::home() as default controller!', 500, 'Route error: missing default controller');
730
731
        $defaultController = self::$defaultController->compiled;
0 ignored issues
show
Bug introduced by
The property compiled does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
732
        $defaultController = $defaultController[key($defaultController)];
733
734
        $routes['default_controller'] = $defaultController;
735
736
        if (is_null(self::$_404page))
737
        {
738
            $routes['404_override'] = '';
739
        } else
740
        {
741
            $routes['404_override'] = self::$_404page->controller;
0 ignored issues
show
Bug introduced by
The property controller does not seem to exist. Did you mean defaultController?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
742
        }
743
744
        $routes['translate_uri_dashes'] = self::$translateDashes;
745
746
        return $routes;
747
    }
748
749
750
    /**
751
     * Creates a group of routes with common attributes
752
     *
753
     * @param  array $attr set of global attributes
754
     * @param  callback $routes wich contains a set of Route methods
755
     *
756
     * @return void
757
     *
758
     * @access public
759
     * @static
760
     */
761
    public static function group($attr, $routes)
762
    {
763
        if (!is_array($attr))
764
            show_error('Group attribute must be a valid array');
765
766
        if (!isset($attr['prefix']))
767
            show_error('You must specify an prefix!');
768
769
        self::$prefix[] = $attr['prefix'];
770
771
        if (isset($attr['namespace']))
772
        {
773
            self::$namespace[] = $attr['namespace'];
774
        }
775
776
        if (isset($attr['hideOriginals']) && $attr['hideOriginals'] === TRUE)
777
        {
778
            self::$hideOriginals[] = TRUE;
779
        } else
780
        {
781
            self::$hideOriginals[] = FALSE;
782
        }
783
784
        $mcount = 0;
785
        if (isset($attr['middleware']))
786
        {
787
            if (is_array($attr['middleware']) || is_string($attr['middleware']))
788
            {
789
                if (is_array($attr['middleware']) && !empty($attr['middleware']))
790
                {
791
                    $mcount = count($attr['middleware']);
792
                    foreach ($attr['middleware'] as $middleware)
793
                        self::$middleware[] = $middleware;
794
                }
795
                else
796
                {
797
                    self::$middleware[] = $attr['middleware'];
798
                    $mcount = 1;
799
                }
800
            }
801
            else
802
            {
803
                show_error('Group middleware must be an array o a string', 500, 'Route error');
804
            }
805
        }
806
807
        $res = $routes->__invoke();
0 ignored issues
show
Bug introduced by
The method __invoke cannot be called on $routes (of type callable).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
Unused Code introduced by
$res is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
808
809
        array_pop(self::$prefix);
810
        array_pop(self::$hideOriginals);
811
812
        if( isset($attr['namespace']) )
813
            array_pop(self::$namespace);
814
815
        // Flushing nested middleware:
816
        for($i = 0; $i < $mcount; $i++)
817
            array_pop(self::$middleware);
818
    }
819
820
821
    /**
822
     * Creates the 'default_controller' key in CodeIgniter's route array
823
     *
824
     *
825
     * @return void
826
     *
827
     * @access public
828
     * @static
829
     */
830
    public static function home($controller, $as = 'home', $attr = NULL)
831
    {
832
        $routeAttr =
833
            [
834
                'uses' => $controller,
835
                'as'   => $as
836
            ];
837
838
        if (!is_null($attr) && !is_array($attr))
839
            show_error('Default controller attributes must be an array', 500, 'Route error: bad attribute type');
840
841
        if (!is_null($attr))
842
            $routeAttr = array_merge($routeAttr, $attr);
0 ignored issues
show
Unused Code introduced by
$routeAttr is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
843
844
        if (isset($attr['prefix']))
845
            show_error('Default controller may not have a prefix!', 500, 'Route error: prefix not allowed');
846
847
        self::$defaultController = self::$routes[] = self::add('GET', '/', ['uses' => $controller, 'as' => $as], TRUE, TRUE);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to self::$routes[] is correct as self::add('GET', '/', ar...s' => $as), TRUE, TRUE) (which targets Luthier\Core\Route::add()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
848
    }
849
850
851
    /**
852
     * Get all the improved routes defined
853
     *
854
     * @return array List of all defined routes
855
     *
856
     * @access public
857
     * @static
858
     */
859
    public static function getRoutes($verb = NULL)
860
    {
861
        if (is_null($verb))
862
        {
863
            return self::$routes;
864
        }
865
        else
866
        {
867
            $routes = [];
868
            foreach (self::$routes as $route)
0 ignored issues
show
Bug introduced by
The expression self::$routes of type object<Luthier\Core\Route> is not traversable.
Loading history...
869
            {
870
                if ($route->verb == $verb)
871
                    $routes[] = $route;
872
            }
873
            return $routes;
874
        }
875
    }
876
877
878
    /**
879
     * Get all hidden routes
880
     *
881
     * @return Route
882
     *
883
     * @access public
884
     * @static
885
     */
886
    public static function getHiddenRoutes()
887
    {
888
        return self::$hiddenRoutes;
889
    }
890
891
892
    /**
893
     * Retrieve a route wich is called $search (if exists)
894
     *
895
     * @param  string $search The route name to search
896
     * @param  $args (Optional) The route arguments that will be parsed
897
     *
898
     * @return mixed Founded route in case of success, and error in case of no matches.
899
     *
900
     * @access public
901
     * @static
902
     */
903
    public static function getRouteByName($search)
904
    {
905
        $founded = NULL;
906
907
        $args = func_get_args();
908
        unset($args[0]);
909
910
        foreach (self::$routes as $route)
0 ignored issues
show
Bug introduced by
The expression self::$routes of type object<Luthier\Core\Route> is not traversable.
Loading history...
911
        {
912
            if ($route->name == $search)
913
            {
914
                $founded = $route;
915
            }
916
        }
917
918
        if (!is_null($founded))
919
        {
920
            $routeArgs        = self::compileRoute($founded)->args;
921
            $routeArgCount    = count($routeArgs['required']) + count($routeArgs['optional']);
922
            $routeReqArgCount = count($routeArgs['required']);
923
924
            if (count($args) < $routeReqArgCount)
925
            {
926
                $missingArgs = $routeReqArgCount - count($args);
927
                throw new \Exception('Missing '.$missingArgs.' required argument'.($missingArgs != 1 ? 's' : '').' for route "'.$founded->name.'"');
928
            }
929
            if (count($args) > $routeArgCount)
930
            {
931
                throw new \Exception('The route "'.$founded->name.'" expects maximum '.$routeArgCount.' argument'.($routeArgCount != 1 ? 's' : '').', '.count($args).' provided');
932
            }
933
934
            $path = self::compileRoute($founded)->path;
935
936
            foreach ($args as $replacement)
937
            {
938
                $path = preg_replace('/\((.*?)\)/', $replacement, $path, 1);
939
            }
940
941
            $argsLeft = $routeArgCount - count($args);
942
943
            for ($i = $argsLeft; $i >= 0; $i--)
944
            {
945
                $path = preg_replace('/\((.*?)\)/', '', $path, 1);
946
            }
947
948
            return base_url(trim($path, '/'));
949
        }
950
951
        throw new \Exception('The route "'.$search.'" is not defined');
952
    }
953
954
955
    /**
956
     *  Heuristic testing of current uri_string in compiled routes
957
     *
958
     *  This is the 'reverse' process of the improved routing, it'll take the current
959
     *  uri string and attempts to find a CodeIgniter route that matches with his pattern
960
     *
961
     *
962
     * @param Middleware $path
963
     * @param string $requestMethod
964
     * @return mixed
965
     */
966
    public static function getRouteByPath($path, $requestMethod = NULL)
0 ignored issues
show
Coding Style introduced by
getRouteByPath uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
967
    {
968
        if (is_null($requestMethod))
969
            $requestMethod = $_SERVER['REQUEST_METHOD'];
970
971
        $routes = self::getRoutes($requestMethod);
972
973
        if (empty($routes))
974
            return FALSE;
975
976
        $path = trim($path);
977
978
        if ($path == '')
979
            return self::$defaultController;
980
981
        $wildcards =
982
            [
983
                '/\(:any\)/',
984
                '/\(:num\)/',
985
                '/\((.*?)\)/',
986
            ];
987
988
        $replaces =
989
            [
990
                '[^/]+',
991
                '[0-9]+',
992
                '(.*)'
993
            ];
994
995
        foreach (['exact', 'regex'] as $mode)
996
        {
997
            foreach ([$path, $path.'/index'] as $findPath)
998
            {
999
                foreach ($routes as $route)
0 ignored issues
show
Bug introduced by
The expression $routes of type object<Luthier\Core\Route>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1000
                {
1001
                    $compiledPath = key($route->compiled);
1002
1003
                    if ($mode == 'exact')
1004
                    {
1005
                        if ($findPath == $compiledPath)
1006
                            return $route;
1007
                    }
1008
                    else
1009
                    {
1010
                        $e_findPath     = explode('/', $findPath);
1011
                        $e_compiledPath = explode('/', $compiledPath);
1012
1013
                        $c_e_findPath     = count($e_findPath);
1014
                        $c_e_compiledPath = count($e_compiledPath);
1015
1016
                        if ($c_e_findPath == $c_e_compiledPath)
1017
                        {
1018
                            $valid    = TRUE;
1019
                            $skip_seg = [];
1020
1021
                            for ($i = 0; $i < $c_e_findPath; $i++)
1022
                            {
1023
                                $count = 0;
1024
                                $reg   = preg_replace($wildcards, $replaces, $e_compiledPath[$i], -1, $count);
1025
1026
                                $valid = (bool) preg_match('#^'.$reg.'$#', $e_findPath[$i]);
1027
1028
                                if ($valid && $count > 0)
1029
                                    $skip_seg[] = $i;
1030
                            }
1031
1032
                            if ($valid)
1033
                            {
1034
                                for ($i = 0; $i < $c_e_findPath; $i++)
1035
                                {
1036
                                    if(in_array($i, $skip_seg))
1037
                                        continue;
1038
1039
                                    if ($valid)
1040
                                        $valid = $e_findPath[$i] == $e_compiledPath[$i];
1041
                                }
1042
                            }
1043
1044
                            if ($valid)
1045
                                return $route;
1046
                        }
1047
                    }
1048
                }
1049
            }
1050
        }
1051
1052
        return FALSE;
1053
    }
1054
1055
1056
    /**
1057
     * Parse improved route arguments by a provided path
1058
     *
1059
     * @param  object  $route
1060
     * @param  string  $path
1061
     *
1062
     * @return bool | object
1063
     *
1064
     * @access public
1065
     * @static
1066
     */
1067
    public static function getRouteArgs($route, $path)
1068
    {
1069
        $compiled = self::compileRoute($route);
0 ignored issues
show
Documentation introduced by
$route is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1070
1071
        $r_seg = explode('/', $compiled->path);
1072
        $p_seg = explode('/', $path);
1073
1074
        $args   = [];
1075
        $n_args = 1;
1076
1077
        for ($s = 0; $s < count($r_seg); $s++)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1078
        {
1079
            if (!isset($p_seg[$s]))
1080
                continue;
1081
1082
            if ($r_seg[$s] != $p_seg[$s])
1083
            {
1084
                $args['$'.$n_args] = $p_seg[$s];
1085
                $n_args++;
1086
            }
1087
        }
1088
1089
        return $args;
1090
    }
1091
1092
1093
    /**
1094
     * Returns an array with the valid HTTP Verbs used in routes
1095
     *
1096
     * @return Route
1097
     *
1098
     * @access public
1099
     * @static
1100
     */
1101
    public static function getHTTPVerbs()
1102
    {
1103
        return self::$http_verbs;
1104
    }
1105
1106
1107
    /**
1108
     * Set the 404 error controller ($route['404_override'])
1109
     *
1110
     * @param  string  $controller
1111
     *
1112
     * @return void
1113
     *
1114
     * @access public
1115
     * @static
1116
     */
1117
    public static function set404($controller, $path = '404')
1118
    {
1119
        self::$_404page = (object)
1120
        [
1121
            'controller' => $controller,
1122
            'path'       => $path
1123
        ];
1124
    }
1125
1126
1127
    /**
1128
     * Get the 404 route
1129
     *
1130
     * @return Route $_404page
1131
     *
1132
     * @return Route | null
1133
     *
1134
     * @access public
1135
     * @static
1136
     */
1137
    public static function get404()
1138
    {
1139
        return self::$_404page;
1140
    }
1141
1142
1143
    /**
1144
     * Set the 'translate_uri_dashes' value ($route['translate_uri_dashes'])
1145
     *
1146
     * @param  $value
1147
     *
1148
     * @return void
1149
     *
1150
     * @access public
1151
     * @static
1152
     */
1153
    public static function setTrasnlateUriDashes($value)
1154
    {
1155
        self::$translateDashes = (bool) $value;
0 ignored issues
show
Documentation Bug introduced by
It seems like (bool) $value of type boolean is incompatible with the declared type object<Luthier\Core\Route> of property $translateDashes.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1156
    }
1157
1158
1159
    /**
1160
     * Attempts to trigger a nice 404 view (if a custom 404 controller is defined)
1161
     *
1162
     * @return void
1163
     *
1164
     * @access public
1165
     * @static
1166
     */
1167
    public static function trigger404()
1168
    {
1169
        if (!is_null(self::$_404page))
1170
        {
1171
            header('Location: '.config_item('base_url').self::$_404page->path);
0 ignored issues
show
Bug introduced by
The property path does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1172
            die;
0 ignored issues
show
Coding Style Compatibility introduced by
The method trigger404() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
1173
        }
1174
1175
        show_404();
1176
    }
1177
}