Completed
Push — master ( 0d861e...0d54f8 )
by Stefano
03:27
created

Route::group()   C

Complexity

Conditions 11
Paths 50

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 2 Features 0
Metric Value
cc 11
eloc 22
c 5
b 2
f 0
nc 50
nop 2
dl 0
loc 39
rs 5.2653

How to fix   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
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
                  $tags           = [],
21
                  $optimized_tree = [];
22
23
    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...
24
                  $pattern            = '',
25
                  $matcher_pattern    = '',
26
                  $dynamic            = false,
27
                  $callback           = null,
28
                  $methods            = [],
29
                  $befores            = [],
30
                  $afters             = [],
31
32
                  $rules              = [],
33
                  $response           = '',
34
                  $tag                = '';
35
36
37
    /**
38
     * Create a new route definition. This method permits a fluid interface.
39
     *
40
     * @param string $URLPattern The URL pattern, can be used named parameters for variables extraction
41
     * @param $callback The callback to invoke on route match.
42
     * @param string $method The HTTP method for which the route must respond.
43
     * @return Route
44
     */
45
    public function __construct($URLPattern, $callback = null, $method='get'){
46
      $prefix  = static::$prefix ? rtrim(implode('',static::$prefix),'/') : '';
47
      $pattern = '/' . trim($URLPattern, "/");
48
49
      $this->callback         = $callback;
50
51
      // Adjust / optionality with dynamic patterns
52
      // Ex:  /test/(:a) ===> /test(/:a)
53
      $this->URLPattern       = str_replace('//','/',str_replace('/(','(/', rtrim("{$prefix}{$pattern}","/")));
54
55
      $this->dynamic          = $this->isDynamic($this->URLPattern);
56
57
      $this->pattern          = $this->dynamic
58
                                ? $this->compilePatternAsRegex($this->URLPattern, $this->rules)
59
                                : $this->URLPattern;
60
61
      $this->matcher_pattern  = $this->dynamic
62
                                ? $this->compilePatternAsRegex($this->URLPattern, $this->rules, false)
63
                                : '';
64
65
      // We will use hash-checks, for O(1) complexity vs O(n)
66
      $this->methods[$method] = 1;
67
      return static::add($this);
68
    }
69
70
    /**
71
     * Check if route match on a specified URL and HTTP Method.
72
     * @param  [type] $URL The URL to check against.
73
     * @param  string $method The HTTP Method to check against.
74
     * @return boolean
75
     */
76
    public function match($URL, $method='get'){
77
      $method = strtolower($method);
78
79
      // * is an http method wildcard
80
      if (empty($this->methods[$method]) && empty($this->methods['*'])) return false;
81
82
      return (bool) (
83
        $this->dynamic
84
           ? preg_match($this->matcher_pattern, '/'.trim($URL,'/'))
85
           : rtrim($URL,'/') == rtrim($this->pattern,'/')
86
      );
87
    }
88
89
    /**
90
     * Clears all stored routes definitions to pristine conditions.
91
     * @return void
92
     */
93
    public static function reset(){
94
      static::$routes = [];
95
      static::$base   = '';
96
      static::$prefix = [];
97
      static::$group  = [];
98
      static::$optimized_tree = [];
99
    }
100
101
    /**
102
     * Run one of the mapped callbacks to a passed HTTP Method.
103
     * @param  array  $args The arguments to be passed to the callback
104
     * @param  string $method The HTTP Method requested.
105
     * @return array The callback response.
106
     */
107
    public function run(array $args, $method='get'){
108
      $method = strtolower($method);
109
      $append_echoed_text = Options::get('core.route.append_echoed_text',true);
110
111
      // Call direct befores
112 View Code Duplication
      if ( $this->befores ) {
113
        // Reverse befores order
114
        foreach (array_reverse($this->befores) as $mw) {
115
          static::trigger('before', $this, $mw);
116
          Event::trigger('core.route.before', $this, $mw);
117
          ob_start();
118
          $mw_result  = call_user_func($mw);
119
          $raw_echoed = ob_get_clean();
120
          if ($append_echoed_text) Response::add($raw_echoed);
121
          if ( false  === $mw_result ) {
122
            return [''];
123
          } else {
124
            Response::add($mw_result);
125
          }
126
        }
127
      }
128
129
      $callback = (is_array($this->callback) && isset($this->callback[$method]))
130
                  ? $this->callback[$method]
131
                  : $this->callback;
132
133
      if (is_callable($callback)) {
134
        Response::type( Options::get('core.route.response_default_type', Response::TYPE_HTML) );
135
136
        ob_start();
137
        $view_results = call_user_func_array($callback, $args);
138
        $raw_echoed   = ob_get_clean();
139
140
        if ($append_echoed_text) Response::add($raw_echoed);
141
        Response::add($view_results);
142
      }
143
144
      // Apply afters
145 View Code Duplication
      if ( $this->afters ) {
146
        foreach ($this->afters as $mw) {
147
          static::trigger('after', $this, $mw);
148
          Event::trigger('core.route.after', $this, $mw);
149
          ob_start();
150
          $mw_result  = call_user_func($mw);
151
          $raw_echoed = ob_get_clean();
152
          if ($append_echoed_text) Response::add($raw_echoed);
153
          if ( false  === $mw_result ) {
154
            return [''];
155
          } else {
156
            Response::add($mw_result);
157
          }
158
        }
159
      }
160
161
      static::trigger('end', $this);
162
      Event::trigger('core.route.end', $this);
163
164
      return [Filter::with('core.route.response', Response::body())];
165
     }
166
167
    /**
168
     * Check if route match URL and HTTP Method and run if it is valid.
169
     * @param  [type] $URL The URL to check against.
170
     * @param  string $method The HTTP Method to check against.
171
     * @return array The callback response.
172
     */
173
    public function runIfMatch($URL, $method='get'){
174
      return $this->match($URL,$method) ? $this->run($this->extractArgs($URL),$method) : null;
175
    }
176
177
    /**
178
     * Start a route definition, default to HTTP GET.
179
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
180
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
181
     * @return Route
182
     */
183
    public static function on($URLPattern, $callback = null){
184
      return new Route($URLPattern,$callback);
185
    }
186
187
    /**
188
     * Start a route definition with HTTP Method via GET.
189
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
190
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
191
     * @return Route
192
     */
193
    public static function get($URLPattern, $callback = null){
194
      return (new Route($URLPattern,$callback))->via('get');
195
    }
196
197
    /**
198
     * Start a route definition with HTTP Method via POST.
199
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
200
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
201
     * @return Route
202
     */
203
    public static function post($URLPattern, $callback = null){
204
      return (new Route($URLPattern,$callback))->via('post');
205
    }
206
207
    /**
208
     * Start a route definition, for any HTTP Method (using * wildcard).
209
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
210
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
211
     * @return Route
212
     */
213
    public static function any($URLPattern, $callback = null){
214
      return (new Route($URLPattern,$callback))->via('*');
215
    }
216
217
    /**
218
     * Bind a callback to the route definition
219
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
220
     * @return Route
221
     */
222
    public function & with($callback){
223
      $this->callback = $callback;
224
      return $this;
225
    }
226
227
    /**
228
     * Bind a middleware callback to invoked before the route definition
229
     * @param  callable $before The callback to be invoked ($this is binded to the route object).
230
     * @return Route
231
     */
232
    public function & before($callback){
233
      $this->befores[] = $callback;
234
      return $this;
235
    }
236
237
    /**
238
     * Bind a middleware callback to invoked after the route definition
239
     * @param  $callback The callback to be invoked ($this is binded to the route object).
240
     * @return Route
241
     */
242
    public function & after($callback){
243
      $this->afters[] = $callback;
244
      return $this;
245
    }
246
247
    /**
248
     * Defines the HTTP Methods to bind the route onto.
249
     *
250
     * Example:
251
     * <code>
252
     *  Route::on('/test')->via('get','post','delete');
253
     * </code>
254
     *
255
     * @return Route
256
     */
257
    public function & via(...$methods){
258
      $this->methods = [];
259
      foreach ($methods as $method){
260
        $this->methods[strtolower($method)] = true;
261
      }
262
      return $this;
263
    }
264
265
    /**
266
     * Defines the regex rules for the named parameter in the current URL pattern
267
     *
268
     * Example:
269
     * <code>
270
     *  Route::on('/proxy/:number/:url')
271
     *    ->rules([
272
     *      'number'  => '\d+',
273
     *      'url'     => '.+',
274
     *    ]);
275
     * </code>
276
     *
277
     * @param  array  $rules The regex rules
278
     * @return Route
279
     */
280
    public function & rules(array $rules){
281
      foreach ((array)$rules as $varname => $rule){
282
        $this->rules[$varname] = $rule;
283
      }
284
      $this->pattern         = $this->compilePatternAsRegex( $this->URLPattern, $this->rules );
285
      $this->matcher_pattern = $this->compilePatternAsRegex( $this->URLPattern, $this->rules, false );
286
      return $this;
287
    }
288
289
    /**
290
     * Map a HTTP Method => callable array to a route.
291
     *
292
     * Example:
293
     * <code>
294
     *  Route::map('/test'[
295
     *      'get'     => function(){ echo "HTTP GET"; },
296
     *      'post'    => function(){ echo "HTTP POST"; },
297
     *      'put'     => function(){ echo "HTTP PUT"; },
298
     *      'delete'  => function(){ echo "HTTP DELETE"; },
299
     *    ]);
300
     * </code>
301
     *
302
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
303
     * @param  array $callbacks The HTTP Method => callable map.
304
     * @return Route
305
     */
306
    public static function & map($URLPattern, $callbacks = []){
307
      $route           = new static($URLPattern);
308
      $route->callback = [];
309
      foreach ($callbacks as $method => $callback) {
310
        $method = strtolower($method);
311
        if (Request::method() !== $method) continue;
312
        $route->callback[$method] = $callback;
313
        $route->methods[$method]  = 1;
314
      }
315
      return $route;
316
    }
317
318
    /**
319
     * Assign a name tag to the route
320
     * @param  string $name The name tag of the route.
321
     * @return Route
322
     */
323
    public function & tag($name){
324
      if ($this->tag = $name) static::$tags[$name] =& $this;
325
      return $this;
326
    }
327
328
    /**
329
     * Reverse routing : obtain a complete URL for a named route with passed parameters
330
     * @param  array $params The parameter map of the route dynamic values.
331
     * @return URL
332
     */
333
    public function getURL($params = []){
334
      $params = (array)$params;
335
      return new URL(rtrim(preg_replace('(/+)','/',preg_replace_callback('(:(\w+))',function($m) use ($params){
336
        return isset($params[$m[1]]) ? $params[$m[1]].'/' : '';
337
      },strtr($this->URLPattern,['('=>'',')'=>'']))),'/')?:'/');
338
    }
339
340
    /**
341
     * Get a named route
342
     * @param  string $name The name tag of the route.
343
     * @return Route or false if not found
344
     */
345
    public static function as($name){
346
      return isset(static::$tags[$name]) ? static::$tags[$name] : false;
347
    }
348
349
   /**
350
     * Helper for reverse routing : obtain a complete URL for a named route with passed parameters
351
     * @param  string $name The name tag of the route.
352
     * @param  array $params The parameter map of the route dynamic values.
353
     * @return string
354
     */
355
    public static function URL($name, $params = []){
356
      return ($r = static::as($name)) ? $r-> getURL($params) : new URL();
357
    }
358
359
    /**
360
     * Compile an URL schema to a PREG regular expression.
361
     * @param  string $pattern The URL schema.
362
     * @return string The compiled PREG RegEx.
363
     */
364
    protected static function compilePatternAsRegex($pattern, $rules=[], $extract_params=true){
365
366
      return '#^'.preg_replace_callback('#:([a-zA-Z]\w*)#',$extract_params
367
        // Extract named parameters
368
        ? function($g) use (&$rules){
369
            return '(?<' . $g[1] . '>' . (isset($rules[$g[1]])?$rules[$g[1]]:'[^/]+') .')';
370
          }
371
        // Optimized for matching
372
        : function($g) use (&$rules){
373
            return isset($rules[$g[1]]) ? $rules[$g[1]] : '[^/]+';
374
          },
375
      str_replace(['.',')','*'],['\.',')?','.+'],$pattern)).'$#';
376
    }
377
378
    /**
379
     * Extract the URL schema variables from the passed URL.
380
     * @param  string  $pattern The URL schema with the named parameters
381
     * @param  string  $URL The URL to process, if omitted the current request URI will be used.
382
     * @param  boolean $cut If true don't limit the matching to the whole URL (used for group pattern extraction)
383
     * @return array The extracted variables
384
     */
385
    protected static function extractVariablesFromURL($pattern, $URL=null, $cut=false){
386
      $URL     = $URL ?: Request::URI();
387
      $pattern = $cut ? str_replace('$#','',$pattern).'#' : $pattern;
388
      $args    = [];
389
      if ( !preg_match($pattern,'/'.trim($URL,'/'),$args) ) return false;
390
      foreach ($args as $key => $value) {
391
        if (false === is_string($key)) unset($args[$key]);
392
      }
393
      return $args;
394
    }
395
396
397
    public function extractArgs($URL){
398
      $args = [];
399
      if ( $this->dynamic ) {
400
        preg_match($this->pattern, '/'.trim($URL,'/'), $args);
401
        foreach ($args as $key => $value) {
402
          if (false === is_string($key)) unset($args[$key]);
403
        }
404
      }
405
      return $args;
406
    }
407
408
    /**
409
     * Check if an URL schema need dynamic matching (regex).
410
     * @param  string  $pattern The URL schema.
411
     * @return boolean
412
     */
413
    protected static function isDynamic($pattern){
414
      return strlen($pattern) != strcspn($pattern,':(?[*+');
415
    }
416
417
    /**
418
     * Add a route to the internal route repository.
419
     * @param Route $route
420
     * @return Route
421
     */
422
    public static function add($route){
423
      if (is_a($route, 'Route')){
424
425
        // Add to tag map
426
        if ($route->tag) static::$tags[$route->tag] =& $route;
427
428
        // Optimize tree
429
        if (Options::get('core.route.auto_optimize', true)){
430
          $base =& static::$optimized_tree;
431
          foreach (explode('/',trim(preg_replace('#^(.+?)\(?:.+$#','$1',$route->URLPattern),'/')) as $segment) {
432
            $segment = trim($segment,'(');
433
            if (!isset($base[$segment])) $base[$segment] = [];
434
            $base =& $base[$segment];
435
          }
436
          $base[] =& $route;
437
        }
438
      }
439
440
      // Add route to active group
441
      if ( isset(static::$group[0]) ) static::$group[0]->add($route);
442
443
      return static::$routes[implode('', static::$prefix)][] = $route;
444
    }
445
446
    /**
447
     * Define a route group, if not immediately matched internal code will not be invoked.
448
     * @param  string $prefix The url prefix for the internal route definitions.
449
     * @param  string $callback This callback is invoked on $prefix match of the current request URI.
450
     */
451
    public static function group($prefix, $callback){
452
453
      // Skip definition if current request doesn't match group.
454
      $pre_prefix = rtrim(implode('',static::$prefix),'/');
455
      $URI   = Request::URI();
456
      $args  = [];
457
      $group = false;
458
459
      switch (true) {
460
461
        // Dynamic group
462
        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...
463
          $args = static::extractVariablesFromURL($prx=static::compilePatternAsRegex("$pre_prefix$prefix"), null, true);
464
          if ( $args !== false ) {
465
            // Burn-in $prefix as static string
466
            $partial = preg_match_all(str_replace('$#', '#', $prx), $URI, $partial) ? $partial[0][0] : '';
467
            $prefix = $partial ? preg_replace('#^'.implode('',static::$prefix).'#', '', $partial) : $prefix;
468
          }
469
470
        // Static group
471
        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...
472
             || ( ! Options::get('core.route.pruning', true) ) :
473
474
          static::$prefix[] = $prefix;
475
          if (empty(static::$group)) static::$group = [];
476
          array_unshift(static::$group, $group = new RouteGroup());
477
478
          // Call the group body function
479
          call_user_func_array($callback, $args ?: []);
480
481
          array_shift(static::$group);
482
          array_pop(static::$prefix);
483
          if (empty(static::$prefix)) static::$prefix = [''];
484
        break;
485
486
      }
487
488
      return $group ?: new RouteGroup();
489
    }
490
491
    public static function exitWithError($code, $message="Application Error"){
492
      Response::error($code,$message);
493
      Response::send();
494
      exit;
495
    }
496
497
    /**
498
     * Start the route dispatcher and resolve the URL request.
499
     * @param  string $URL The URL to match onto.
500
     * @param  string $method The HTTP method.
501
     * @param  bool $return_route If setted to true it will *NOT* execute the route but it will return her.
502
     * @return boolean true if a route callback was executed.
503
     */
504
    public static function dispatch($URL=null, $method=null, $return_route=false){
505
        if (!$URL)     $URL     = Request::URI();
506
        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...
507
508
        $__deferred_send = new Deferred(function(){
509
          if (Options::get('core.response.autosend',true)){
510
            Response::send();
511
          }
512
        });
513
514
        if (empty(static::$optimized_tree)) {
515 View Code Duplication
          foreach ((array)static::$routes as $group => $routes){
516
              foreach ($routes as $route) {
517
                  if (is_a($route, 'Route') && $route->match($URL,$method)){
518
                    if ($return_route){
519
                      return $route;
520
                    } else {
521
                      $route->run($route->extractArgs($URL),$method);
522
                      return true;
523
                    }
524
                  }
525
              }
526
          }
527
        } else {
528
          $routes =& static::$optimized_tree;
529
          foreach (explode('/',trim($URL,'/')) as $segment) {
530
            if (isset($routes[$segment])) $routes =& $routes[$segment];
531
              // Root-level dynamic routes Ex: "/:param"
532
              else if (isset($routes[''])) $routes =& $routes[''];
533
            else break;
534
          }
535 View Code Duplication
          if (isset($routes[0]) && !is_array($routes[0])) foreach ((array)$routes as $route) {
536
              if ($route->match($URL, $method)){
537
                    if ($return_route){
538
                      return $route;
539
                    } else {
540
                      $route->run($route->extractArgs($URL),$method);
541
                      return true;
542
                    }
543
              }
544
          }
545
        }
546
547
        Response::status(404, '404 Resource not found.');
548
        foreach (array_filter(array_merge(
549
          (static::trigger(404)?:[]),
550
          (Event::trigger(404)?:[])
551
        )) as $res){
552
           Response::add($res);
553
        }
554
        return false;
555
    }
556
}
557
558
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...
559
  protected $routes;
560
561
  public function __construct(){
562
    $this->routes = new SplObjectStorage;
563
    return Route::add($this);
564
  }
565
566
  public function has($r){
567
    return $this->routes->contains($r);
568
  }
569
570
  public function add($r){
571
    $this->routes->attach($r);
572
    return $this;
573
  }
574
575
  public function remove($r){
576
    if ($this->routes->contains($r)) $this->routes->detach($r);
577
    return $this;
578
  }
579
580
  public function before($callbacks){
581
    foreach ($this->routes as $route){
582
      $route->before($callbacks);
583
    }
584
    return $this;
585
  }
586
587
  public function after($callbacks){
588
    foreach ($this->routes as $route){
589
      $route->after($callbacks);
590
    }
591
    return $this;
592
  }
593
594
}
595
596