Completed
Push — master ( 14ebf3...093445 )
by Stefano
06:50
created

Route   D

Complexity

Total Complexity 91

Size/Duplication

Total Lines 488
Duplicated Lines 10.86 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 16
Bugs 5 Features 2
Metric Value
c 16
b 5
f 2
dl 53
loc 488
rs 4.8717
wmc 91
lcom 1
cbo 9

23 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 24 4
A match() 0 12 4
A reset() 0 7 1
C run() 31 59 13
A runIfMatch() 0 3 2
A on() 0 3 1
A get() 0 3 1
A post() 0 3 1
A any() 0 3 1
A with() 0 4 1
A before() 0 4 1
A after() 0 4 1
A via() 0 7 2
A rules() 0 8 2
A map() 0 11 3
A compilePatternAsRegex() 0 13 4
B extractVariablesFromURL() 0 10 6
A extractArgs() 0 10 4
A isDynamic() 0 3 1
B add() 0 13 6
C group() 0 39 11
A exitWithError() 0 5 1
C dispatch() 22 49 20

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Route often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Route, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Route
5
 *
6
 * URL Router and action dispatcher.
7
 *
8
 * @package core
9
 * @author [email protected]
10
 * @copyright Caffeina srl - 2016 - http://caffeina.com
11
 */
12
13
class Route {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
14
    use Module, Events;
15
16
    public static $routes,
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
17
                  $base       = '',
18
                  $prefix     = [],
19
                  $group      = [],
20
                  $optimized_tree = [];
21
22
    protected $URLPattern      = '',
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
23
              $pattern         = '',
24
              $matcher_pattern = '',
25
              $dynamic         = false,
26
              $callback        = null,
27
              $methods         = [],
28
              $befores         = [],
29
              $afters          = [],
30
31
              $rules           = [],
32
              $response        = '';
33
34
35
    /**
36
     * Create a new route definition. This method permits a fluid interface.
37
     *
38
     * @param string $URLPattern The URL pattern, can be used named parameters for variables extraction
39
     * @param $callback The callback to invoke on route match.
40
     * @param string $method The HTTP method for which the route must respond.
41
     * @return Route
42
     */
43
    public function __construct($URLPattern, $callback = null, $method='get'){
44
      $prefix  = static::$prefix ? rtrim(implode('',static::$prefix),'/') : '';
45
      $pattern = '/' . trim($URLPattern, "/");
46
47
      $this->callback         = $callback;
48
49
      // Adjust / optionality with dynamic patterns
50
      // Ex:  /test/(:a) ===> /test(/:a)
51
      $this->URLPattern       = str_replace('//','/',str_replace('/(','(/', rtrim("{$prefix}{$pattern}","/")));
52
53
      $this->dynamic          = $this->isDynamic($this->URLPattern);
54
55
      $this->pattern          = $this->dynamic
56
                                ? $this->compilePatternAsRegex($this->URLPattern, $this->rules)
57
                                : $this->URLPattern;
58
59
      $this->matcher_pattern  = $this->dynamic
60
                                ? $this->compilePatternAsRegex($this->URLPattern, $this->rules, false)
61
                                : '';
62
63
      // We will use hash-checks, for O(1) complexity vs O(n)
64
      $this->methods[$method] = 1;
65
      return static::add($this);
66
    }
67
68
    /**
69
     * Check if route match on a specified URL and HTTP Method.
70
     * @param  [type] $URL The URL to check against.
71
     * @param  string $method The HTTP Method to check against.
72
     * @return boolean
73
     */
74
    public function match($URL, $method='get'){
75
      $method = strtolower($method);
76
77
      // * is an http method wildcard
78
      if (empty($this->methods[$method]) && empty($this->methods['*'])) return false;
79
80
      return (bool) (
81
        $this->dynamic
82
           ? preg_match($this->matcher_pattern, '/'.trim($URL,'/'))
83
           : rtrim($URL,'/') == rtrim($this->pattern,'/')
84
      );
85
    }
86
87
    /**
88
     * Clears all stored routes definitions to pristine conditions.
89
     * @return void
90
     */
91
    public static function reset(){
92
      static::$routes = [];
93
      static::$base   = '';
94
      static::$prefix = [];
95
      static::$group  = [];
96
      static::$optimized_tree = [];
97
    }
98
99
    /**
100
     * Run one of the mapped callbacks to a passed HTTP Method.
101
     * @param  array  $args The arguments to be passed to the callback
102
     * @param  string $method The HTTP Method requested.
103
     * @return array The callback response.
104
     */
105
    public function run(array $args, $method='get'){
106
      $method = strtolower($method);
107
      $append_echoed_text = Options::get('core.route.append_echoed_text',true);
108
109
      // Call direct befores
110 View Code Duplication
      if ( $this->befores ) {
111
        // Reverse befores order
112
        foreach (array_reverse($this->befores) as $mw) {
113
          static::trigger('before', $this, $mw);
114
          Event::trigger('core.route.before', $this, $mw);
115
          ob_start();
116
          $mw_result  = call_user_func($mw);
117
          $raw_echoed = ob_get_clean();
118
          if ($append_echoed_text) Response::add($raw_echoed);
119
          if ( false  === $mw_result ) {
120
            return [''];
121
          } else {
122
            Response::add($mw_result);
123
          }
124
        }
125
      }
126
127
      $callback = (is_array($this->callback) && isset($this->callback[$method]))
128
                  ? $this->callback[$method]
129
                  : $this->callback;
130
131
      if (is_callable($callback)) {
132
        Response::type( Options::get('core.route.response_default_type', Response::TYPE_HTML) );
133
134
        ob_start();
135
        $view_results = call_user_func_array($callback, $args);
136
        $raw_echoed   = ob_get_clean();
137
138
        if ($append_echoed_text) Response::add($raw_echoed);
139
        Response::add($view_results);
140
      }
141
142
      // Apply afters
143 View Code Duplication
      if ( $this->afters ) {
144
        foreach ($this->afters as $mw) {
145
          static::trigger('after', $this, $mw);
146
          Event::trigger('core.route.after', $this, $mw);
147
          ob_start();
148
          $mw_result  = call_user_func($mw);
149
          $raw_echoed = ob_get_clean();
150
          if ($append_echoed_text) Response::add($raw_echoed);
151
          if ( false  === $mw_result ) {
152
            return [''];
153
          } else {
154
            Response::add($mw_result);
155
          }
156
        }
157
      }
158
159
      static::trigger('end', $this);
160
      Event::trigger('core.route.end', $this);
161
162
      return [Filter::with('core.route.response', Response::body())];
163
     }
164
165
    /**
166
     * Check if route match URL and HTTP Method and run if it is valid.
167
     * @param  [type] $URL The URL to check against.
168
     * @param  string $method The HTTP Method to check against.
169
     * @return array The callback response.
170
     */
171
    public function runIfMatch($URL, $method='get'){
172
      return $this->match($URL,$method) ? $this->run($this->extractArgs($URL),$method) : null;
173
    }
174
175
    /**
176
     * Start a route definition, default to HTTP GET.
177
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
178
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
179
     * @return Route
180
     */
181
    public static function on($URLPattern, $callback = null){
182
      return new Route($URLPattern,$callback);
183
    }
184
185
    /**
186
     * Start a route definition with HTTP Method via GET.
187
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
188
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
189
     * @return Route
190
     */
191
    public static function get($URLPattern, $callback = null){
192
      return (new Route($URLPattern,$callback))->via('get');
193
    }
194
195
    /**
196
     * Start a route definition with HTTP Method via POST.
197
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
198
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
199
     * @return Route
200
     */
201
    public static function post($URLPattern, $callback = null){
202
      return (new Route($URLPattern,$callback))->via('post');
203
    }
204
205
    /**
206
     * Start a route definition, for any HTTP Method (using * wildcard).
207
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
208
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
209
     * @return Route
210
     */
211
    public static function any($URLPattern, $callback = null){
212
      return (new Route($URLPattern,$callback))->via('*');
213
    }
214
215
    /**
216
     * Bind a callback to the route definition
217
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
218
     * @return Route
219
     */
220
    public function & with($callback){
221
      $this->callback = $callback;
222
      return $this;
223
    }
224
225
    /**
226
     * Bind a middleware callback to invoked before the route definition
227
     * @param  callable $before The callback to be invoked ($this is binded to the route object).
228
     * @return Route
229
     */
230
    public function & before($callback){
231
      $this->befores[] = $callback;
232
      return $this;
233
    }
234
235
    /**
236
     * Bind a middleware callback to invoked after the route definition
237
     * @param  $callback The callback to be invoked ($this is binded to the route object).
238
     * @return Route
239
     */
240
    public function & after($callback){
241
      $this->afters[] = $callback;
242
      return $this;
243
    }
244
245
    /**
246
     * Defines the HTTP Methods to bind the route onto.
247
     *
248
     * Example:
249
     * <code>
250
     *  Route::on('/test')->via('get','post','delete');
251
     * </code>
252
     *
253
     * @return Route
254
     */
255
    public function & via(...$methods){
256
      $this->methods = [];
257
      foreach ($methods as $method){
258
        $this->methods[strtolower($method)] = true;
259
      }
260
      return $this;
261
    }
262
263
    /**
264
     * Defines the regex rules for the named parameter in the current URL pattern
265
     *
266
     * Example:
267
     * <code>
268
     *  Route::on('/proxy/:number/:url')
269
     *    ->rules([
270
     *      'number'  => '\d+',
271
     *      'url'     => '.+',
272
     *    ]);
273
     * </code>
274
     *
275
     * @param  array  $rules The regex rules
276
     * @return Route
277
     */
278
    public function & rules(array $rules){
279
      foreach ((array)$rules as $varname => $rule){
280
        $this->rules[$varname] = $rule;
281
      }
282
      $this->pattern         = $this->compilePatternAsRegex( $this->URLPattern, $this->rules );
283
      $this->matcher_pattern = $this->compilePatternAsRegex( $this->URLPattern, $this->rules, false );
284
      return $this;
285
    }
286
287
    /**
288
     * Map a HTTP Method => callable array to a route.
289
     *
290
     * Example:
291
     * <code>
292
     *  Route::map('/test'[
293
     *      'get'     => function(){ echo "HTTP GET"; },
294
     *      'post'    => function(){ echo "HTTP POST"; },
295
     *      'put'     => function(){ echo "HTTP PUT"; },
296
     *      'delete'  => function(){ echo "HTTP DELETE"; },
297
     *    ]);
298
     * </code>
299
     *
300
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
301
     * @param  array $callbacks The HTTP Method => callable map.
302
     * @return Route
303
     */
304
    public static function & map($URLPattern, $callbacks = []){
305
      $route           = new static($URLPattern);
306
      $route->callback = [];
307
      foreach ($callbacks as $method => $callback) {
308
        $method = strtolower($method);
309
        if (Request::method() !== $method) continue;
310
        $route->callback[$method] = $callback;
311
        $route->methods[$method]  = 1;
312
      }
313
      return $route;
314
    }
315
316
    /**
317
     * Compile an URL schema to a PREG regular expression.
318
     * @param  string $pattern The URL schema.
319
     * @return string The compiled PREG RegEx.
320
     */
321
    protected static function compilePatternAsRegex($pattern, $rules=[], $extract_params=true){
322
323
      return '#^'.preg_replace_callback('#:([a-zA-Z]\w*)#',$extract_params
324
        // Extract named parameters
325
        ? function($g) use (&$rules){
326
            return '(?<' . $g[1] . '>' . (isset($rules[$g[1]])?$rules[$g[1]]:'[^/]+') .')';
327
          }
328
        // Optimized for matching
329
        : function($g) use (&$rules){
330
            return isset($rules[$g[1]]) ? $rules[$g[1]] : '[^/]+';
331
          },
332
      str_replace(['.',')','*'],['\.',')?','.+'],$pattern)).'$#';
333
    }
334
335
    /**
336
     * Extract the URL schema variables from the passed URL.
337
     * @param  string  $pattern The URL schema with the named parameters
338
     * @param  string  $URL The URL to process, if omitted the current request URI will be used.
339
     * @param  boolean $cut If true don't limit the matching to the whole URL (used for group pattern extraction)
340
     * @return array The extracted variables
341
     */
342
    protected static function extractVariablesFromURL($pattern, $URL=null, $cut=false){
343
      $URL     = $URL ?: Request::URI();
344
      $pattern = $cut ? str_replace('$#','',$pattern).'#' : $pattern;
345
      $args    = [];
346
      if ( !preg_match($pattern,'/'.trim($URL,'/'),$args) ) return false;
347
      foreach ($args as $key => $value) {
348
        if (false === is_string($key)) unset($args[$key]);
349
      }
350
      return $args;
351
    }
352
353
354
    public function extractArgs($URL){
355
      $args = [];
356
      if ( $this->dynamic ) {
357
        preg_match($this->pattern, '/'.trim($URL,'/'), $args);
358
        foreach ($args as $key => $value) {
359
          if (false === is_string($key)) unset($args[$key]);
360
        }
361
      }
362
      return $args;
363
    }
364
365
    /**
366
     * Check if an URL schema need dynamic matching (regex).
367
     * @param  string  $pattern The URL schema.
368
     * @return boolean
369
     */
370
    protected static function isDynamic($pattern){
371
      return strlen($pattern) != strcspn($pattern,':(?[*+');
372
    }
373
374
    /**
375
     * Add a route to the internal route repository.
376
     * @param Route $route
377
     * @return Route
378
     */
379
    public static function add($route){
380
      if (Options::get('core.route.auto_optimize', true) && is_a($route, 'Route')){
381
        $base =& static::$optimized_tree;
382
        foreach (explode('/',trim(preg_replace('#^(.+?)\(?:.+$#','$1',$route->URLPattern),'/')) as $segment) {
383
          $segment = trim($segment,'(');
384
          if (!isset($base[$segment])) $base[$segment] = [];
385
          $base =& $base[$segment];
386
        }
387
        $base[] = $route;
388
      }
389
      if ( isset(static::$group[0]) ) static::$group[0]->add($route);
390
      return static::$routes[implode('', static::$prefix)][] = $route;
391
    }
392
393
    /**
394
     * Define a route group, if not immediately matched internal code will not be invoked.
395
     * @param  string $prefix The url prefix for the internal route definitions.
396
     * @param  string $callback This callback is invoked on $prefix match of the current request URI.
397
     */
398
    public static function group($prefix, $callback){
399
400
      // Skip definition if current request doesn't match group.
401
      $pre_prefix = rtrim(implode('',static::$prefix),'/');
402
      $URI   = Request::URI();
403
      $args  = [];
404
      $group = false;
405
406
      switch (true) {
407
408
        // Dynamic group
409
        case static::isDynamic($prefix) :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
410
          $args = static::extractVariablesFromURL($prx=static::compilePatternAsRegex("$pre_prefix$prefix"), null, true);
411
          if ( $args !== false ) {
412
            // Burn-in $prefix as static string
413
            $partial = preg_match_all(str_replace('$#', '#', $prx), $URI, $partial) ? $partial[0][0] : '';
414
            $prefix = $partial ? preg_replace('#^'.implode('',static::$prefix).'#', '', $partial) : $prefix;
415
          }
416
417
        // Static group
418
        case ( 0 === strpos("$URI/", "$pre_prefix$prefix/") )
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
419
             || ( ! Options::get('core.route.pruning', true) ) :
420
421
          static::$prefix[] = $prefix;
422
          if (empty(static::$group)) static::$group = [];
423
          array_unshift(static::$group, $group = new RouteGroup());
424
425
          // Call the group body function
426
          call_user_func_array($callback, $args ?: []);
427
428
          array_shift(static::$group);
429
          array_pop(static::$prefix);
430
          if (empty(static::$prefix)) static::$prefix = [''];
431
        break;
432
433
      }
434
435
      return $group ?: new RouteGroup();
436
    }
437
438
    public static function exitWithError($code, $message="Application Error"){
439
      Response::error($code,$message);
440
      Response::send();
441
      exit;
442
    }
443
444
    /**
445
     * Start the route dispatcher and resolve the URL request.
446
     * @param  string $URL The URL to match onto.
447
     * @param  string $method The HTTP method.
448
     * @param  bool $return_route If setted to true it will *NOT* execute the route but it will return her.
449
     * @return boolean true if a route callback was executed.
450
     */
451
    public static function dispatch($URL=null, $method=null, $return_route=false){
452
        if (!$URL)     $URL     = Request::URI();
453
        if (!$method)  $method  = Request::method();
0 ignored issues
show
Bug Best Practice introduced by
The expression $method of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
454
455
        $__deferred_send = new Deferred(function(){
456
          if (Options::get('core.response.autosend',true)){
457
            Response::send();
458
          }
459
        });
460
461
        if (empty(static::$optimized_tree)) {
462 View Code Duplication
          foreach ((array)static::$routes as $group => $routes){
463
              foreach ($routes as $route) {
464
                  if (is_a($route, 'Route') && $route->match($URL,$method)){
465
                    if ($return_route){
466
                      return $route;
467
                    } else {
468
                      $route->run($route->extractArgs($URL),$method);
469
                      return true;
470
                    }
471
                  }
472
              }
473
          }
474
        } else {
475
          $routes =& static::$optimized_tree;
476
          foreach (explode('/',trim($URL,'/')) as $segment) {
477
            if (isset($routes[$segment])) $routes =& $routes[$segment]; else break;
478
          }
479 View Code Duplication
          if (isset($routes[0]) && !is_array($routes[0])) foreach ((array)$routes as $route) {
480
              if ($route->match($URL, $method)){
481
                    if ($return_route){
482
                      return $route;
483
                    } else {
484
                      $route->run($route->extractArgs($URL),$method);
485
                      return true;
486
                    }
487
              }
488
          }
489
        }
490
491
        Response::status(404, '404 Resource not found.');
492
        foreach (array_filter(array_merge(
493
          (static::trigger(404)?:[]),
494
          (Event::trigger(404)?:[])
495
        )) as $res){
496
           Response::add($res);
497
        }
498
        return false;
499
    }
500
}
501
502
class RouteGroup {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
503
  protected $routes;
504
505
  public function __construct(){
506
    $this->routes = new SplObjectStorage;
507
    return Route::add($this);
508
  }
509
510
  public function has($r){
511
    return $this->routes->contains($r);
512
  }
513
514
  public function add($r){
515
    $this->routes->attach($r);
516
    return $this;
517
  }
518
519
  public function remove($r){
520
    if ($this->routes->contains($r)) $this->routes->detach($r);
521
    return $this;
522
  }
523
524
  public function before($callbacks){
525
    foreach ($this->routes as $route){
526
      $route->before($callbacks);
527
    }
528
    return $this;
529
  }
530
531
  public function after($callbacks){
532
    foreach ($this->routes as $route){
533
      $route->after($callbacks);
534
    }
535
    return $this;
536
  }
537
538
}
539
540