Completed
Push — master ( 8ad409...e42510 )
by Stefano
02:53
created

Route::exitWithError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 1
eloc 4
c 3
b 1
f 0
nc 1
nop 2
dl 0
loc 5
rs 9.4285
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
    public static function reset(){
83
      static::$routes = [];
84
      static::$base   = '',
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

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