Completed
Push — master ( e42510...75d07b )
by Stefano
04:13
created

RouteGroup::remove()   A

Complexity

Conditions 2
Paths 2

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