Completed
Push — master ( 814dc8...ebf47d )
by
unknown
03:14
created

Route::post()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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