Completed
Push — master ( 1ded02...260d88 )
by Stefano
03:45
created

Route::via()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 5
nc 2
nop 0
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 - 2015 - http://caffeina.it
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;
15
16
    protected 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
        $this->URLPattern = rtrim(implode('',static::$prefix),'/') . '/' . trim($URLPattern,'/')?:'/';
43
        $this->URLPattern = $this->URLPattern != '/' ? rtrim($this->URLPattern,'/') : $this->URLPattern;
44
        $this->dynamic    = $this->isDynamic($this->URLPattern);
45
        $this->pattern    = $this->dynamic ? $this->compilePatternAsRegex($this->URLPattern, $this->rules) : $this->URLPattern;
46
        $this->callback   = $callback;
47
48
        // We will use hash-checks, for O(1) complexity vs O(n)
49
        $this->methods[$method] = 1;
50
        return static::add($this);
51
    }
52
53
    /**
54
     * Check if route match on a specified URL and HTTP Method.
55
     * @param  [type] $URL The URL to check against.
56
     * @param  string $method The HTTP Method to check against.
57
     * @return boolean
58
     */
59
    public function match($URL,$method='get'){
60
        $method = strtolower($method);
61
62
        // * is an http method wildcard
63
        if (empty($this->methods[$method]) && empty($this->methods['*'])) return false;
64
        $URL = rtrim($URL,'/');
65
        $args = [];
66
        if ( $this->dynamic
67
               ? preg_match($this->pattern,$URL,$args)
68
               : $URL == rtrim($this->pattern,'/')
69
        ){
70
            foreach ($args as $key => $value) {
71
              if ( false === is_string($key) ) unset($args[$key]);
72
            }
73
            return $args;
74
        }
75
        return false;
76
    }
77
78
    /**
79
     * Run one of the mapped callbacks to a passed HTTP Method.
80
     * @param  array  $args The arguments to be passed to the callback
81
     * @param  string $method The HTTP Method requested.
82
     * @return array The callback response.
83
     */
84
    public function run(array $args, $method='get'){
85
        $method = strtolower($method);
86
        $this->response 			 		= '';
87
        $this->response_object 		= null;
88
       	$this->response_is_object = false;
89
90
        // Call direct befores
91 View Code Duplication
        if ( $this->befores ) {
92
          // Reverse befores order
93
          foreach (array_reverse($this->befores) as $mw) {
94
	        	ob_start();
95
            $mw_result = call_user_func($mw->bindTo($this));
96
          	$this->response .= ob_get_clean();
97
            if ( false  === $mw_result ) {
98
            	return [''];
99
            } else if (is_a($mw_result,'View') || is_string($mw_result)) {
100
              $this->response .= (string)$mw_result;
101
          	}
102
          }
103
        }
104
105
        Event::trigger('core.route.before', $this);
106
107
        $callback = (is_array($this->callback) && isset($this->callback[$method]))
108
                    ? $this->callback[$method] : $this->callback;
109
110
        if (is_callable($callback)) {
111
          // Capure callback output
112
					Response::type(Response::TYPE_HTML);
113
	        ob_start();
114
	        // Silence "Cannot bind an instance to a static closure" warnings
115
	        $view_results 	 = call_user_func_array(@$callback->bindTo($this), $args);
116
	        $this->response .= ob_get_clean();
117
118
	        // Render View if returned, else echo string or encode json response
119 View Code Duplication
	        if ( null !== $view_results ) {
120
	          if (is_a($view_results,'View') || is_string($view_results)) {
121
	              $this->response .= (string)$view_results;
122
	          } else {
123
			        	$this->response_is_object = true;
124
	              $this->response_object 		= $view_results;
125
	          }
126
	        }
127
128 View Code Duplication
        } else if (is_a($callback,'View') || is_string($callback)) {
129
          // return rendered View or direct string
130
        	$this->response .= (string)$callback;
131
        } else {
132
          // JSON encode returned value
133
        	$this->response_is_object = true;
134
        	$this->response_object 		= $callback;
135
        }
136
137
        // Apply afters
138 View Code Duplication
        if ( $this->afters ) {
139
          foreach ($this->afters as $mw) {
140
	        	ob_start();
141
            $mw_result = call_user_func($mw->bindTo($this));
142
          	$this->response .= ob_get_clean();
143
            if ( false  === $mw_result ) {
144
            	return [''];
145
            } else if (is_a($mw_result,'View') || is_string($mw_result)) {
146
              $this->response .= (string)$mw_result;
147
          	}
148
          }
149
        }
150
151
        Event::trigger('core.route.after', $this);
152
153
        if ( $this->response_is_object ){
154
					$this->response = Response::json($this->response_object);
155
        } else {
156
					Response::add($this->response);
157
        }
158
159
        Event::trigger('core.route.end', $this);
160
161
        return [Filter::with('core.route.response', $this->response)];
162
    }
163
164
    /**
165
     * Check if route match URL and HTTP Method and run if it is valid.
166
     * @param  [type] $URL The URL to check against.
167
     * @param  string $method The HTTP Method to check against.
168
     * @return array The callback response.
169
     */
170
    public function runIfMatch($URL, $method='get'){
171
        return ($args = $this->match($URL,$method)) ? $this->run($args,$method) : null;
172
    }
173
174
    /**
175
     * Start a route definition, default to HTTP GET.
176
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
177
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
178
     * @return Route
179
     */
180
    public static function on($URLPattern, $callback = null){
181
        return new Route($URLPattern,$callback);
182
    }
183
184
    /**
185
     * Start a route definition, for any HTTP Method (using * wildcard).
186
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
187
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
188
     * @return Route
189
     */
190
    public static function any($URLPattern, $callback = null){
191
        return (new Route($URLPattern,$callback))->via('*');
192
    }
193
194
    /**
195
     * Bind a callback to the route definition
196
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
197
     * @return Route
198
     */
199
    public function & with($callback){
200
        $this->callback = $callback;
201
        return $this;
202
    }
203
204
    /**
205
     * Bind a middleware callback to invoked before the route definition
206
     * @param  callable $before The callback to be invoked ($this is binded to the route object).
207
     * @return Route
208
     */
209
    public function & before($callback){
210
        $this->befores[] = $callback;
211
        return $this;
212
    }
213
214
    /**
215
     * Bind a middleware callback to invoked after the route definition
216
     * @param  $callback The callback to be invoked ($this is binded to the route object).
217
     * @return Route
218
     */
219
    public function & after($callback){
220
        $this->afters[] = $callback;
221
        return $this;
222
    }
223
224
    /**
225
     * Defines the HTTP Methods to bind the route onto.
226
     *
227
     * Example:
228
     * <code>
229
     *  Route::on('/test')->via('get','post','delete');
230
     * </code>
231
     *
232
     * @return Route
233
     */
234
    public function & via(){
235
        $this->methods = [];
236
        foreach (func_get_args() as $method){
237
            $this->methods[strtolower($method)] = true;
238
        }
239
        return $this;
240
    }
241
242
    /**
243
     * Defines the regex rules for the named parameter in the current URL pattern
244
     *
245
     * Example:
246
     * <code>
247
     *  Route::on('/proxy/:number/:url')
248
     *    ->rules([
249
     *      'number'  => '\d+',
250
     *      'url'     => '.+',
251
     *    ]);
252
     * </code>
253
     *
254
     * @param  array  $rules The regex rules
255
     * @return Route
256
     */
257
    public function & rules(array $rules){
258
        foreach ((array)$rules as $varname => $rule){
259
            $this->rules[$varname] = $rule;
260
        }
261
        $this->pattern = $this->compilePatternAsRegex($this->URLPattern,$this->rules);
262
        return $this;
263
    }
264
265
    /**
266
     * Map a HTTP Method => callable array to a route.
267
     *
268
     * Example:
269
     * <code>
270
     *  Route::map('/test'[
271
     *      'get'     => function(){ echo "HTTP GET"; },
272
     *      'post'    => function(){ echo "HTTP POST"; },
273
     *      'put'     => function(){ echo "HTTP PUT"; },
274
     *      'delete'  => function(){ echo "HTTP DELETE"; },
275
     *    ]);
276
     * </code>
277
     *
278
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
279
     * @param  array $callbacks The HTTP Method => callable map.
280
     * @return Route
281
     */
282
    public static function & map($URLPattern, $callbacks = []){
283
        $route           = new static($URLPattern);
284
        $route->callback = [];
285
        foreach ($callbacks as $method => $callback) {
286
           $method = strtolower($method);
287
           if (Request::method() !== $method) continue;
288
           $route->callback[$method] = $callback;
289
           $route->methods[$method]  = 1;
290
        }
291
        return $route;
292
    }
293
294
    /**
295
     * Compile an URL schema to a PREG regular expression.
296
     * @param  string $pattern The URL schema.
297
     * @return string The compiled PREG RegEx.
298
     */
299
    protected static function compilePatternAsRegex($pattern, $rules=[]){
300
        return '#^'.preg_replace_callback('#:([a-zA-Z]\w*)#S',function($g) use (&$rules){
301
            return '(?<' . $g[1] . '>' . (isset($rules[$g[1]])?$rules[$g[1]]:'[^/]+') .')';
302
        },str_replace(['.',')','*'],['\.',')?','.+'],$pattern)).'$#';
303
    }
304
305
    /**
306
     * Extract the URL schema variables from the passed URL.
307
     * @param  string  $pattern The URL schema with the named parameters
308
     * @param  string  $URL The URL to process, if omitted the current request URI will be used.
309
     * @param  boolean $cut If true don't limit the matching to the whole URL (used for group pattern extraction)
310
     * @return array The extracted variables
311
     */
312
    protected static function extractVariablesFromURL($pattern, $URL=null, $cut=false){
313
        $URL     = $URL ?: Request::URI();
314
        $pattern = $cut ? str_replace('$#','',$pattern).'#' : $pattern;
315
        if ( !preg_match($pattern,$URL,$args) ) return false;
316
        foreach ($args as $key => $value) {
317
            if (false === is_string($key)) unset($args[$key]);
318
        }
319
        return $args;
320
    }
321
322
    /**
323
     * Check if an URL schema need dynamic matching (regex).
324
     * @param  string  $pattern The URL schema.
325
     * @return boolean
326
     */
327
    protected static function isDynamic($pattern){
328
      return strlen($pattern) != strcspn($pattern,':(?[*+');
329
    }
330
331
    /**
332
     * Add a route to the internal route repository.
333
     * @param Route $r
334
     * @return Route
335
     */
336
    public static function add($r){
337
        if ( isset(static::$group[0]) ) static::$group[0]->add($r);
338
        return static::$routes[implode('',static::$prefix)][] = $r;
339
    }
340
341
    /**
342
     * Define a route group, if not immediatly matched internal code will not be invoked.
343
     * @param  string $prefix The url prefix for the internal route definitions.
344
     * @param  string $callback This callback is invoked on $prefix match of the current request URI.
345
     */
346
    public static function group($prefix,$callback=null){
347
348
        // Skip definition if current request doesn't match group.
349
350
        $prefix_complete = rtrim(implode('',static::$prefix),'/') . $prefix;
351
352
        if ( static::isDynamic($prefix) ){
353
354
            // Dynamic group, capture vars
355
            $vars = static::extractVariablesFromURL(static::compilePatternAsRegex($prefix_complete),null,true);
356
357
            // Errors in compile pattern or variable extraction, aborting.
358
            if (false === $vars) return;
359
360
            static::$prefix[] = $prefix;
361
            if (empty(static::$group)) static::$group = [];
362
            array_unshift(static::$group, new RouteGroup());
363
            if ($callback) call_user_func_array($callback,$vars);
364
            $group = static::$group[0];
365
            array_shift(static::$group);
366
            array_pop(static::$prefix);
367
            if (empty(static::$prefix)) static::$prefix=[''];
368
            return $group;
369
370
        } else if ( 0 === strpos(Request::URI(), $prefix_complete) ){
371
372
            // Static group
373
            static::$prefix[] = $prefix;
374
            if (empty(static::$group)) static::$group = [];
375
            array_unshift(static::$group, new RouteGroup());
376
            if ($callback) call_user_func($callback);
377
            $group = static::$group[0];
378
            array_shift(static::$group);
379
            array_pop(static::$prefix);
380
            if (empty(static::$prefix)) static::$prefix=[''];
381
            return $group;
382
        } else {
383
384
            // Null Object
385
            return new RouteGroup();
386
        }
387
388
    }
389
390
    public static function exitWithError($code,$message="Application Error"){
391
    	Response::error($code,$message);
392
    	Response::send();
393
    	exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method exitWithError() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
394
    }
395
396
    /**
397
     * Start the route dispatcher and resolve the URL request.
398
     * @param  string $URL The URL to match onto.
399
     * @return boolean true if a route callback was executed.
400
     */
401
    public static function dispatch($URL=null,$method=null){
402
        if (!$URL)     $URL     = Request::URI();
403
        if (!$method)  $method  = Request::method();
404
405
        $__deferred_send = new Deferred(function(){
1 ignored issue
show
Unused Code introduced by
$__deferred_send is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
406
          if (Options::get('core.response.autosend',false)){
407
            Response::send();
408
          }
409
        });
410
411
        foreach ((array)static::$routes as $group => $routes){
412
            foreach ($routes as $route) {
413
                if (is_a($route, 'Route') && false !== ($args = $route->match($URL,$method))){
414
                    $route->run($args,$method);
415
                    return true;
416
                }
417
            }
418
        }
419
420
        Response::status(404, '404 Resource not found.');
421
        Event::trigger(404);
422
        return false;
423
    }
424
}
425
426
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...
427
  protected $routes;
428
429
  public function __construct(){
430
    $this->routes = new SplObjectStorage;
431
    return Route::add($this);
432
  }
433
434
  public function has($r){
435
    return $this->routes->contains($r);
436
  }
437
438
  public function add($r){
439
    $this->routes->attach($r);
440
    return $this;
441
  }
442
443
  public function remove($r){
444
    if ($this->routes->contains($r)) $this->routes->detach($r);
445
    return $this;
446
  }
447
448
  public function before($callbacks){
449
    foreach ($this->routes as $route){
450
      $route->before($callbacks);
451
    }
452
    return $this;
453
  }
454
455
  public function after($callbacks){
456
    foreach ($this->routes as $route){
457
      $route->after($callbacks);
458
    }
459
    return $this;
460
  }
461
462
}
463
464