Completed
Push — master ( 05e8ae...5c8da7 )
by Stefano
03:12
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 - 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
     * Run one of the mapped callbacks to a passed HTTP Method.
84
     * @param  array  $args The arguments to be passed to the callback
85
     * @param  string $method The HTTP Method requested.
86
     * @return array The callback response.
87
     */
88
    public function run(array $args, $method='get'){
89
      $method = strtolower($method);
90
      $append_echoed_text = Options::get('core.route.append_echoed_text',true);
91
92
      // Call direct befores
93 View Code Duplication
      if ( $this->befores ) {
94
        // Reverse befores order
95
        foreach (array_reverse($this->befores) as $mw) {
96
          static::trigger('before', $this, $mw);
97
          Event::trigger('core.route.before', $this, $mw);
98
          ob_start();
99
          $mw_result  = call_user_func($mw);
100
          $raw_echoed = ob_get_clean();
101
          if ($append_echoed_text) Response::add($raw_echoed);
102
          if ( false  === $mw_result ) {
103
            return [''];
104
          } else {
105
            Response::add($mw_result);
106
          }
107
        }
108
      }
109
110
      $callback = (is_array($this->callback) && isset($this->callback[$method]))
111
                  ? $this->callback[$method]
112
                  : $this->callback;
113
114
      if (is_callable($callback)) {
115
        Response::type( Options::get('core.route.response_default_type', Response::TYPE_HTML) );
116
117
        ob_start();
118
        $view_results = call_user_func_array($callback, $args);
119
        $raw_echoed   = ob_get_clean();
120
121
        if ($append_echoed_text) Response::add($raw_echoed);
122
        Response::add($view_results);
123
      }
124
125
      // Apply afters
126 View Code Duplication
      if ( $this->afters ) {
127
        foreach ($this->afters as $mw) {
128
          static::trigger('after', $this, $mw);
129
          Event::trigger('core.route.after', $this, $mw);
130
          ob_start();
131
          $mw_result  = call_user_func($mw);
132
          $raw_echoed = ob_get_clean();
133
          if ($append_echoed_text) Response::add($raw_echoed);
134
          if ( false  === $mw_result ) {
135
            return [''];
136
          } else {
137
            Response::add($mw_result);
138
          }
139
        }
140
      }
141
142
      static::trigger('end', $this);
143
      Event::trigger('core.route.end', $this);
144
145
      return [Filter::with('core.route.response', Response::body())];
146
     }
147
148
    /**
149
     * Check if route match URL and HTTP Method and run if it is valid.
150
     * @param  [type] $URL The URL to check against.
151
     * @param  string $method The HTTP Method to check against.
152
     * @return array The callback response.
153
     */
154
    public function runIfMatch($URL, $method='get'){
155
      return ($args = $this->match($URL,$method)) ? $this->run($args,$method) : null;
156
    }
157
158
    /**
159
     * Start a route definition, default to HTTP GET.
160
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
161
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
162
     * @return Route
163
     */
164
    public static function on($URLPattern, $callback = null){
165
      return new Route($URLPattern,$callback);
166
    }
167
168
    /**
169
     * Start a route definition with HTTP Method via GET.
170
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
171
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
172
     * @return Route
173
     */
174
    public static function get($URLPattern, $callback = null){
175
      return (new Route($URLPattern,$callback))->via('get');
176
    }
177
178
    /**
179
     * Start a route definition with HTTP Method via POST.
180
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
181
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
182
     * @return Route
183
     */
184
    public static function post($URLPattern, $callback = null){
185
      return (new Route($URLPattern,$callback))->via('post');
186
    }
187
188
    /**
189
     * Start a route definition, for any HTTP Method (using * wildcard).
190
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
191
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
192
     * @return Route
193
     */
194
    public static function any($URLPattern, $callback = null){
195
      return (new Route($URLPattern,$callback))->via('*');
196
    }
197
198
    /**
199
     * Bind a callback to the route definition
200
     * @param  $callback The callback to be invoked (with variables extracted from the route if present) when the route match the request URI.
201
     * @return Route
202
     */
203
    public function & with($callback){
204
      $this->callback = $callback;
205
      return $this;
206
    }
207
208
    /**
209
     * Bind a middleware callback to invoked before the route definition
210
     * @param  callable $before The callback to be invoked ($this is binded to the route object).
211
     * @return Route
212
     */
213
    public function & before($callback){
214
      $this->befores[] = $callback;
215
      return $this;
216
    }
217
218
    /**
219
     * Bind a middleware callback to invoked after the route definition
220
     * @param  $callback The callback to be invoked ($this is binded to the route object).
221
     * @return Route
222
     */
223
    public function & after($callback){
224
      $this->afters[] = $callback;
225
      return $this;
226
    }
227
228
    /**
229
     * Defines the HTTP Methods to bind the route onto.
230
     *
231
     * Example:
232
     * <code>
233
     *  Route::on('/test')->via('get','post','delete');
234
     * </code>
235
     *
236
     * @return Route
237
     */
238
    public function & via(...$methods){
239
      $this->methods = [];
240
      foreach ($methods as $method){
241
        $this->methods[strtolower($method)] = true;
242
      }
243
      return $this;
244
    }
245
246
    /**
247
     * Defines the regex rules for the named parameter in the current URL pattern
248
     *
249
     * Example:
250
     * <code>
251
     *  Route::on('/proxy/:number/:url')
252
     *    ->rules([
253
     *      'number'  => '\d+',
254
     *      'url'     => '.+',
255
     *    ]);
256
     * </code>
257
     *
258
     * @param  array  $rules The regex rules
259
     * @return Route
260
     */
261
    public function & rules(array $rules){
262
      foreach ((array)$rules as $varname => $rule){
263
        $this->rules[$varname] = $rule;
264
      }
265
      $this->pattern = $this->compilePatternAsRegex( $this->URLPattern, $this->rules );
266
      return $this;
267
    }
268
269
    /**
270
     * Map a HTTP Method => callable array to a route.
271
     *
272
     * Example:
273
     * <code>
274
     *  Route::map('/test'[
275
     *      'get'     => function(){ echo "HTTP GET"; },
276
     *      'post'    => function(){ echo "HTTP POST"; },
277
     *      'put'     => function(){ echo "HTTP PUT"; },
278
     *      'delete'  => function(){ echo "HTTP DELETE"; },
279
     *    ]);
280
     * </code>
281
     *
282
     * @param  string $URLPattern The URL to match against, you can define named segments to be extracted and passed to the callback.
283
     * @param  array $callbacks The HTTP Method => callable map.
284
     * @return Route
285
     */
286
    public static function & map($URLPattern, $callbacks = []){
287
      $route           = new static($URLPattern);
288
      $route->callback = [];
289
      foreach ($callbacks as $method => $callback) {
290
        $method = strtolower($method);
291
        if (Request::method() !== $method) continue;
292
        $route->callback[$method] = $callback;
293
        $route->methods[$method]  = 1;
294
      }
295
      return $route;
296
    }
297
298
    /**
299
     * Compile an URL schema to a PREG regular expression.
300
     * @param  string $pattern The URL schema.
301
     * @return string The compiled PREG RegEx.
302
     */
303
    protected static function compilePatternAsRegex($pattern, $rules=[]){
304
      return '#^'.preg_replace_callback('#:([a-zA-Z]\w*)#S',function($g) use (&$rules){
305
        return '(?<' . $g[1] . '>' . (isset($rules[$g[1]])?$rules[$g[1]]:'[^/]+') .')';
306
      },str_replace(['.',')','*'],['\.',')?','.+'],$pattern)).'$#';
307
    }
308
309
    /**
310
     * Extract the URL schema variables from the passed URL.
311
     * @param  string  $pattern The URL schema with the named parameters
312
     * @param  string  $URL The URL to process, if omitted the current request URI will be used.
313
     * @param  boolean $cut If true don't limit the matching to the whole URL (used for group pattern extraction)
314
     * @return array The extracted variables
315
     */
316
    protected static function extractVariablesFromURL($pattern, $URL=null, $cut=false){
317
      $URL     = $URL ?: Request::URI();
318
      $pattern = $cut ? str_replace('$#','',$pattern).'#' : $pattern;
319
      if ( !preg_match($pattern,$URL,$args) ) return false;
320
      foreach ($args as $key => $value) {
321
        if (false === is_string($key)) unset($args[$key]);
322
      }
323
      return $args;
324
    }
325
326
    /**
327
     * Check if an URL schema need dynamic matching (regex).
328
     * @param  string  $pattern The URL schema.
329
     * @return boolean
330
     */
331
    protected static function isDynamic($pattern){
332
      return strlen($pattern) != strcspn($pattern,':(?[*+');
333
    }
334
335
    /**
336
     * Add a route to the internal route repository.
337
     * @param Route $r
0 ignored issues
show
Bug introduced by
There is no parameter named $r. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
338
     * @return Route
339
     */
340
    public static function add($route){
341
      if ( isset(static::$group[0]) ) static::$group[0]->add($route);
342
      return static::$routes[implode('', static::$prefix)][] = $route;
343
    }
344
345
    /**
346
     * Define a route group, if not immediately matched internal code will not be invoked.
347
     * @param  string $prefix The url prefix for the internal route definitions.
348
     * @param  string $callback This callback is invoked on $prefix match of the current request URI.
349
     */
350
    public static function group($prefix, $callback){
351
352
      // Skip definition if current request doesn't match group.
353
      $pre_prefix = rtrim(implode('',static::$prefix),'/');
354
      $URI   = Request::URI();
355
      $args  = [];
356
      $group = false;
357
358
      switch (true) {
359
360
        // Dynamic group
361
        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...
362
          $args = static::extractVariablesFromURL($prx=static::compilePatternAsRegex("$pre_prefix$prefix"), null, true);
363
          if ( $args !== false ) {
364
            // Burn-in $prefix as static string
365
            $partial = preg_match_all(str_replace('$#', '#', $prx), $URI, $partial) ? $partial[0][0] : '';
366
            $prefix = $partial ? preg_replace('#^'.implode('',static::$prefix).'#', '', $partial) : $prefix;
367
          }
368
369
        // Static group
370
        case ( 0 === strpos("$URI/", "$pre_prefix$prefix/") )
0 ignored issues
show
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...
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...
371
             || ( ! Options::get('core.route.pruning', true) ) :
372
373
          static::$prefix[] = $prefix;
374
          if (empty(static::$group)) static::$group = [];
375
          array_unshift(static::$group, $group = new RouteGroup());
376
377
          // Call the group body function
378
          call_user_func_array($callback, $args ?: []);
379
          
380
          array_shift(static::$group);
381
          array_pop(static::$prefix);
382
          if (empty(static::$prefix)) static::$prefix = [''];
383
        break;
384
385
      }
386
387
      return $group ?: new RouteGroup();
388
    }
389
390
    public static function exitWithError($code,$message="Application Error"){
391
      Response::error($code,$message);
392
      Response::send();
393
      exit;
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(){
406
          if (Options::get('core.response.autosend',true)){
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
        foreach (array_filter(array_merge(
422
          (static::trigger(404)?:[]),
423
          (Event::trigger(404)?:[])
424
        )) as $res){
425
           Response::add($res);
426
        }
427
        return false;
428
    }
429
}
430
431
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...
432
  protected $routes;
433
434
  public function __construct(){
435
    $this->routes = new SplObjectStorage;
436
    return Route::add($this);
437
  }
438
439
  public function has($r){
440
    return $this->routes->contains($r);
441
  }
442
443
  public function add($r){
444
    $this->routes->attach($r);
445
    return $this;
446
  }
447
448
  public function remove($r){
449
    if ($this->routes->contains($r)) $this->routes->detach($r);
450
    return $this;
451
  }
452
453
  public function before($callbacks){
454
    foreach ($this->routes as $route){
455
      $route->before($callbacks);
456
    }
457
    return $this;
458
  }
459
460
  public function after($callbacks){
461
    foreach ($this->routes as $route){
462
      $route->after($callbacks);
463
    }
464
    return $this;
465
  }
466
467
}
468
469