sterzycom/kirby/toolkit/lib/router.php

284 lines
6.8 KiB
PHP

<?php
/**
* Router
*
* The router makes it possible to react
* on any incoming URL scheme
*
* Partly inspired by Laravel's router
*
* @package Kirby Toolkit
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
class Router {
// request instance
protected $request = null;
// the matched route if found
protected $route = null;
// all registered routes
protected $routes = array(
'GET' => array(),
'POST' => array(),
'HEAD' => array(),
'PUT' => array(),
'PATCH' => array(),
'DELETE' => array()
);
// The wildcard patterns supported by the router.
protected $patterns = array(
'(:num)' => '([0-9]+)',
'(:alpha)' => '([a-zA-Z]+)',
'(:any)' => '([a-zA-Z0-9\.\-_%=]+)',
'(:all)' => '(.*)',
);
// The optional wildcard patterns supported by the router.
protected $optional = array(
'/(:num?)' => '(?:/([0-9]+)',
'/(:alpha?)' => '(?:/([a-zA-Z]+)',
'/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%=]+)',
'/(:all?)' => '(?:/(.*)',
);
// additional events, which can be triggered by routes
protected $filters = array();
/**
* Constructor
*
* @param array $routes
*/
public function __construct($routes = array()) {
$this->register($routes);
}
/**
* Returns the found route
*
* @return mixed
*/
public function route() {
return $this->route;
}
/**
* Returns the arguments array from the current route
*
* @return array
*/
public function arguments() {
if($route = $this->route()) return $route->arguments();
}
/**
* Adds a new route
*
* @param mixed $pattern
* @param mixed $params
* @param mixed $optional
* @return Obj
*/
public function register($pattern, $params = array(), $optional = array()) {
if($pattern === false) {
return false;
} else if(is_array($pattern)) {
foreach($pattern as $v) {
if($v === false || empty($v['pattern'])) {
continue;
} else if(is_array($v['pattern'])) {
foreach($v['pattern'] as $p) {
$v['pattern'] = $p;
$this->register($p, $v);
}
} else {
$this->register($v['pattern'], $v);
}
}
return $this;
}
$defaults = array(
'pattern' => $pattern,
'https' => false,
'ajax' => false,
'filter' => null,
'method' => 'GET',
'arguments' => array(),
);
$route = new Obj(array_merge($defaults, $params, $optional));
// convert single methods or methods separated by | to arrays
if(is_string($route->method)) {
if(strpos($route->method, '|') !== false) {
$route->method = str::split($route->method, '|');
} else if($route->method == 'ALL') {
$route->method = array_keys($this->routes);
} else {
$route->method = array($route->method);
}
}
if(is_string($route->filter)) {
if(strpos($route->filter, '|') !== false) {
$route->filter = str::split($route->filter, '|');
} else {
$route->filter = array($route->filter);
}
}
foreach($route->method as $method) {
$this->routes[strtoupper($method)][$route->pattern] = $route;
}
return $route;
}
/**
* Add a new router filter
*
* @param string $name A simple name for the filter, which can be used by routes later
* @param closure $function A filter function, which should be called before routes
*/
public function filter($name, $function) {
$this->filters[$name] = $function;
}
/**
* Return all registered filters
*
* @return array
*/
public function filters() {
return $this->filters;
}
/**
* Call all matching filters
*
* @param mixed $filters
*/
protected function filterer($filters) {
foreach((array)$filters as $filter) {
if(array_key_exists($filter, $this->filters) && is_callable($this->filters[$filter])) {
call_user_func($this->filters[$filter]);
}
}
}
/**
* Returns all added routes
*
* @param string $method
* @return array
*/
public function routes($method = null) {
return is_null($method) ? $this->routes : $this->routes[strtoupper($method)];
}
/**
* Iterate through every route to find a matching route.
*
* @param string $path Optional path to match against
* @return Route
*/
public function run($path = null) {
$method = r::method();
$ajax = r::ajax();
$https = r::ssl();
$routes = a::get($this->routes, $method, array());
// detect path if not set manually
if($path === null) $path = implode('/', (array)url::fragments(detect::path()));
// empty urls should never happen
if(empty($path)) $path = '/';
foreach($routes as $route) {
if($route->https && !$https) continue;
if($route->ajax && !$ajax) continue;
// handle exact matches
if($route->pattern == $path) {
$this->route = $route;
break;
}
// We only need to check routes with regular expression since all others
// would have been able to be matched by the search for literal matches
// we just did before we started searching.
if(strpos($route->pattern, '(') === false) continue;
$preg = '#^'. $this->wildcards($route->pattern) . '$#u';
// If we get a match we'll return the route and slice off the first
// parameter match, as preg_match sets the first array item to the
// full-text match of the pattern.
if(preg_match($preg, $path, $parameters)) {
$this->route = $route;
$this->route->arguments = array_slice($parameters, 1);
break;
}
}
if($this->route && $this->filterer($this->route->filter) !== false) {
return $this->route;
} else {
return null;
}
}
/**
* Translate route URI wildcards into regular expressions.
*
* @param string $pattern
* @return string
*/
protected function wildcards($pattern) {
$search = array_keys($this->optional);
$replace = array_values($this->optional);
// For optional parameters, first translate the wildcards to their
// regex equivalent, sans the ")?" ending. We'll add the endings
// back on when we know the replacement count.
$pattern = str_replace($search, $replace, $pattern, $count);
if($count > 0) $pattern .= str_repeat(')?', $count);
return strtr($pattern, $this->patterns);
}
/**
* Find a registered route by a field and value
*
* @param string $field
* @param string $value
* @return object
*/
public function findRouteBy($field, $value) {
foreach($this->routes as $method => $routes) {
foreach($routes as $route) {
if($route->$field() == $value) return $route;
}
}
}
}