Completed
Push — master ( 75d07b...4139c3 )
by Stefano
03:46
created

Route::after()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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