Completed
Push — master ( d056e8...c87aa3 )
by Stefano
05:25
created

Route::exitWithError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
dl 0
loc 5
rs 9.4285
c 1
b 1
f 0
cc 1
eloc 4
nc 1
nop 2
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 {
14
    use Module;
15
16
    protected static $routes,
17
                     $base    = '',
18
                     $prefix  = [],
19
                     $group   = [];
20
21
    protected $URLPattern     = '',
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
	        ob_start();
113
	        // Silence "Cannot bind an instance to a static closure" warnings
114
	        $view_results 	 = call_user_func_array(@$callback->bindTo($this), $args);
115
	        $this->response .= ob_get_clean();
116
117
	        // Render View if returned, else echo string or encode json response
118 View Code Duplication
	        if ( null !== $view_results ) {
119
	          if (is_a($view_results,'View') || is_string($view_results)) {
120
	              $this->response .= (string)$view_results;
121
	          } else {
122
			        	$this->response_is_object = true;
123
	              $this->response_object 		= $view_results;
124
	          }
125
	        }
126
127 View Code Duplication
        } else if (is_a($callback,'View') || is_string($callback)) {
128
          // return rendered View or direct string
129
        	$this->response .= (string)$callback;
130
        } else {
131
          // JSON encode returned value
132
        	$this->response_is_object = true;
133
        	$this->response_object 		= $callback;
134
        }
135
136
        // Apply afters
137 View Code Duplication
        if ( $this->afters ) {
138
          foreach ($this->afters as $mw) {
139
	        	ob_start();
140
            $mw_result = call_user_func($mw->bindTo($this));
141
          	$this->response .= ob_get_clean();
142
            if ( false  === $mw_result ) {
143
            	return [''];
144
            } else if (is_a($mw_result,'View') || is_string($mw_result)) {
145
              $this->response .= (string)$mw_result;
146
          	}
147
          }
148
        }
149
150
        Event::trigger('core.route.after', $this);
151
152
        if ( $this->response_is_object ){
153
					$this->response = Response::json($this->response_object);
154
        } else {
155
					Response::html($this->response);
156
        }
157
158
        return [$this->response];
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, for any HTTP Method (using * wildcard).
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 any($URLPattern, $callback = null){
188
        return (new Route($URLPattern,$callback))->via('*');
189
    }
190
191
    /**
192
     * Bind a callback to the route definition
193
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
194
     * @return Route
195
     */
196
    public function & with($callback){
197
        $this->callback = $callback;
198
        return $this;
199
    }
200
201
    /**
202
     * Bind a middleware callback to invoked before the route definition
203
     * @param  callable $before The callback to be invoked ($this is binded to the route object).
204
     * @return Route
205
     */
206
    public function & before($callback){
207
        $this->befores[] = $callback;
208
        return $this;
209
    }
210
211
    /**
212
     * Bind a middleware callback to invoked after the route definition
213
     * @param  $callback The callback to be invoked ($this is binded to the route object).
214
     * @return Route
215
     */
216
    public function & after($callback){
217
        $this->afters[] = $callback;
218
        return $this;
219
    }
220
221
    /**
222
     * Defines the HTTP Methods to bind the route onto.
223
     *
224
     * Example:
225
     * <code>
226
     *  Route::on('/test')->via('get','post','delete');
227
     * </code>
228
     *
229
     * @return Route
230
     */
231
    public function & via(){
232
        $this->methods = [];
233
        foreach (func_get_args() as $method){
234
            $this->methods[strtolower($method)] = true;
235
        }
236
        return $this;
237
    }
238
239
    /**
240
     * Defines the regex rules for the named parameter in the current URL pattern
241
     *
242
     * Example:
243
     * <code>
244
     *  Route::on('/proxy/:number/:url')
245
     *    ->rules([
246
     *      'number'  => '\d+',
247
     *      'url'     => '.+',
248
     *    ]);
249
     * </code>
250
     *
251
     * @param  array  $rules The regex rules
252
     * @return Route
253
     */
254
    public function & rules(array $rules){
255
        foreach ((array)$rules as $varname => $rule){
256
            $this->rules[$varname] = $rule;
257
        }
258
        $this->pattern = $this->compilePatternAsRegex($this->URLPattern,$this->rules);
259
        return $this;
260
    }
261
262
    /**
263
     * Map a HTTP Method => callable array to a route.
264
     *
265
     * Example:
266
     * <code>
267
     *  Route::map('/test'[
268
     *      'get'     => function(){ echo "HTTP GET"; },
269
     *      'post'    => function(){ echo "HTTP POST"; },
270
     *      'put'     => function(){ echo "HTTP PUT"; },
271
     *      'delete'  => function(){ echo "HTTP DELETE"; },
272
     *    ]);
273
     * </code>
274
     *
275
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
276
     * @param  array $callbacks The HTTP Method => callable map.
277
     * @return Route
278
     */
279
    public static function & map($URLPattern, $callbacks = []){
280
        $route           = new static($URLPattern);
281
        $route->callback = [];
282
        foreach ($callbacks as $method => $callback) {
283
           $method = strtolower($method);
284
           if (Request::method() !== $method) continue;
285
           $route->callback[$method] = $callback;
286
           $route->methods[$method]  = 1;
287
        }
288
        return $route;
289
    }
290
291
    /**
292
     * Compile an URL schema to a PREG regular expression.
293
     * @param  string $pattern The URL schema.
294
     * @return string The compiled PREG RegEx.
295
     */
296
    protected static function compilePatternAsRegex($pattern, $rules=[]){
297
        return '#^'.preg_replace_callback('#:([a-zA-Z]\w*)#S',function($g) use (&$rules){
298
            return '(?<' . $g[1] . '>' . (isset($rules[$g[1]])?$rules[$g[1]]:'[^/]+') .')';
299
        },str_replace(['.',')','*'],['\.',')?','.+'],$pattern)).'$#';
300
    }
301
302
    /**
303
     * Extract the URL schema variables from the passed URL.
304
     * @param  string  $pattern The URL schema with the named parameters
305
     * @param  string  $URL The URL to process, if omitted the current request URI will be used.
306
     * @param  boolean $cut If true don't limit the matching to the whole URL (used for group pattern extraction)
307
     * @return array The extracted variables
308
     */
309
    protected static function extractVariablesFromURL($pattern, $URL=null, $cut=false){
310
        $URL     = $URL ?: Request::URI();
311
        $pattern = $cut ? str_replace('$#','',$pattern).'#' : $pattern;
312
        if ( !preg_match($pattern,$URL,$args) ) return false;
313
        foreach ($args as $key => $value) {
314
            if (false === is_string($key)) unset($args[$key]);
315
        }
316
        return $args;
317
    }
318
319
    /**
320
     * Check if an URL schema need dynamic matching (regex).
321
     * @param  string  $pattern The URL schema.
322
     * @return boolean
323
     */
324
    protected static function isDynamic($pattern){
325
      return strlen($pattern) != strcspn($pattern,':(?[*+');
326
    }
327
328
    /**
329
     * Add a route to the internal route repository.
330
     * @param Route $r
331
     * @return Route
332
     */
333
    public static function add($r){
334
        if ( isset(static::$group[0]) ) static::$group[0]->add($r);
335
        return static::$routes[implode('',static::$prefix)][] = $r;
336
    }
337
338
    /**
339
     * Define a route group, if not immediatly matched internal code will not be invoked.
340
     * @param  string $prefix The url prefix for the internal route definitions.
341
     * @param  string $callback This callback is invoked on $prefix match of the current request URI.
342
     */
343
    public static function group($prefix,$callback=null){
344
345
        // Skip definition if current request doesn't match group.
346
347
        $prefix_complete = rtrim(implode('',static::$prefix),'/') . $prefix;
348
349
        if ( static::isDynamic($prefix) ){
350
351
            // Dynamic group, capture vars
352
            $vars = static::extractVariablesFromURL(static::compilePatternAsRegex($prefix_complete),null,true);
353
354
            // Errors in compile pattern or variable extraction, aborting.
355
            if (false === $vars) return;
356
357
            static::$prefix[] = $prefix;
358
            if (empty(static::$group)) static::$group = [];
359
            array_unshift(static::$group, new RouteGroup());
360
            if ($callback) call_user_func_array($callback,$vars);
361
            $group = static::$group[0];
362
            array_shift(static::$group);
363
            array_pop(static::$prefix);
364
            if (empty(static::$prefix)) static::$prefix=[''];
365
            return $group;
366
367
        } else if ( 0 === strpos(Request::URI(), $prefix_complete) ){
368
369
            // Static group
370
            static::$prefix[] = $prefix;
371
            if (empty(static::$group)) static::$group = [];
372
            array_unshift(static::$group, new RouteGroup());
373
            if ($callback) call_user_func($callback);
374
            $group = static::$group[0];
375
            array_shift(static::$group);
376
            array_pop(static::$prefix);
377
            if (empty(static::$prefix)) static::$prefix=[''];
378
            return $group;
379
        } else {
380
381
            // Null Object
382
            return new RouteGroup();
383
        }
384
385
    }
386
387
    public static function exitWithError($code,$message="Application Error"){
388
    	Response::error($code,$message);
389
    	Response::send();
390
    	exit;
391
    }
392
393
    /**
394
     * Start the route dispatcher and resolve the URL request.
395
     * @param  string $URL The URL to match onto.
396
     * @return boolean true if a route callback was executed.
397
     */
398
    public static function dispatch($URL=null,$method=null){
399
        if (!$URL)     $URL     = Request::URI();
400
        if (!$method)  $method  = Request::method();
401
        foreach ((array)static::$routes as $group => $routes){
402
            foreach ($routes as $route) {
403
                if (is_a($route, 'Route') && false !== ($args = $route->match($URL,$method))){
404
                    $route->run($args,$method);
405
                    return true;
406
                }
407
            }
408
        }
409
        Response::status(404, '404 Resource not found.');
410
        Event::trigger(404);
411
        return false;
412
    }
413
}
414
415
class RouteGroup {
416
  protected $routes;
417
418
  public function __construct(){
419
    $this->routes = new SplObjectStorage;
420
    return Route::add($this);
421
  }
422
423
  public function has($r){
424
    return $this->routes->contains($r);
425
  }
426
427
  public function add($r){
428
    $this->routes->attach($r);
429
    return $this;
430
  }
431
432
  public function remove($r){
433
    if ($this->routes->contains($r)) $this->routes->detach($r);
434
    return $this;
435
  }
436
437
  public function before($callbacks){
438
    foreach ($this->routes as $route){
439
      $route->before($callbacks);
440
    }
441
    return $this;
442
  }
443
444
  public function after($callbacks){
445
    foreach ($this->routes as $route){
446
      $route->after($callbacks);
447
    }
448
    return $this;
449
  }
450
451
}
452
453