Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Enso router
<?php
namespace Enso\Core;
/**
* Routes are used to determine the controller and action for a requested URI.
* Every route generates a regular expression which is used to match a URI
* and a route. Routes may also contain keys which can be used to set the
* controller, action, and parameters.
*
* @package Enso\Core
* @copyright (c) 2016-2018 Enso team
* @license MIT License
*/
class Route
{
// What must be escaped in the route regex
const REGEX_ESCAPE = '[.\\+*?[^\\]${}=!|]';
// What can be part of a <segment> value
const REGEX_SEGMENT = '[^/.,;?\n]++';
// Matches a URI group and captures the contents
const REGEX_GROUP = '\(((?:(?>[^()]+)|(?R))*)\)';
// Defines the pattern of a <segment>
const REGEX_KEY = '<([a-zA-Z0-9_]++)>';
/**
* @var string
*/
protected $name;
/**
* @var string URI pattern
*/
protected $uri;
/**
* @var array Key patterns for URI pattern
*/
protected $keys = [
'protocol' => '[a-z]+',
'host' => '[-a-z0-9\.]+',
'namespace' => '[a-zA-Z0-9\\]+',
'controller' => '[a-zA-Z0-9]+',
'action' => '[a-zA-Z0-9]+'
];
/**
* @var string
*/
protected $compiledUri;
/**
* @var array
*/
protected $defaults = [
'protocol' => 'http',
'host' => false,
'namespace' => '',
'controller' => '',
'action' => 'index'
];
/**
* @var callable Callback
*/
protected $filter;
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return string
*/
public function __toString()
{
return $this->name;
}
/**
* @return array
*/
public function getDefaults()
{
return $this->defaults;
}
/**
* @return callable|null
*/
public function getFilter()
{
return $this->filter;
}
/**
* Creates a new route.
*
* @param string $name Route name
* @param string $uri Route URI pattern
* @param array $keys Key patterns for URI pattern
* @param array $defaults Default values for keys
* @param callable $filter Filter object or function for values
* @return void
*/
public function __construct($name, $uri, array $keys = [], array $defaults = [], callable $filter = null)
{
$this->name = strval($name);
$this->uri = strval($uri);
$this->keys = array_merge($this->keys, $keys);
$this->defaults = array_merge($this->defaults, $defaults);
$this->filter = $filter;
}
/**
* Returns the compiled regular expression for the route. This translates
* keys and optional groups to a proper PCRE regular expression.
*
* @return string
*/
protected function getCompiledUri()
{
if (empty($this->compiledUri)) {
// The URI should be considered literal except for keys and optional parts.
// Escape everything preg_quote would escape except for: ( ) < >
$expr = preg_replace('#'.self::REGEX_ESCAPE.'#', '\\\\$0', $this->uri);
if (strpos( $expr, '(') !== false) {
// Make optional parts of the URI non-capturing and optional
$expr = str_replace(['(', ')'], ['(?:', ')?'], $expr);
}
// Insert default regex for keys
$expr = str_replace(['<', '>'], ['(?P<', '>'.self::REGEX_SEGMENT.')'], $expr);
if ($this->keys) {
$search = $replace = [];
foreach ($this->keys as $key => $value) {
$search[] = '<'.$key.'>'.self::REGEX_SEGMENT;
$replace[] = '<'.$key .'>'.$value;
}
// Replace the default regex with the user-specified regex
$expr = str_replace($search, $replace, $expr);
}
// Store the compiled regex locally
$this->compiledUri = '#^'.$expr.'$#uD';
}
return $this->compiledUri;
}
/**
* Tests if the route matches a given request. A successful match will return
* all of the routed parameters as an array, a failed match will return false.
*
* @param Request $request
* @return array|false
*/
public function matches(Request $request)
{
// Get the URI from the Request
$uri = trim($request->uri(), '/');
if (!preg_match($this->getCompiledUri(), $uri, $params)) {
return false;
}
foreach ($params as $key => $value) {
// Delete all unnamed keys
if (is_int($key)) {
unset($params[$key]);
}
}
foreach ($this->defaults as $key => $value) {
if (!isset($params[$key]) || $params[$key] === '') {
// Set default values for any key that was not matched
$params[$key] = $value;
}
}
if ($params['namespace']) {
$params['namespace'] = str_replace(
' ',
'\\',
ucwords(str_replace('\\', ' ', $params['namespace']))
);
}
if ($params['controller']) {
$params['controller'] = ucfirst($params['controller']);
}
if ($this->filter) {
// Execute the filter giving it the route, params, and request
return call_user_func($this->filter, $this, $params, $request);
}
return $params;
}
/**
* Generates a URI for the current route based on the parameters given.
* @param array $params URI parameters
* @param string $protocol Transfer protocol
* @return string
* @throws Exception
*/
public function uri(array $params = [], $protocol = null)
{
if ($params) {
$params = array_map('rawurlencode', $params);
// Decode slashes back, see Apache docs about AllowEncodedSlashes
// and AcceptPathInfo
$params = str_replace(
array('%2F', '%5C'),
array('/', '\\'),
$params
);
// @todo check http server
}
$defaults = $this->defaults;
/**
* Recursively compiles a portion of a URI specification by replacing
* the specified parameters and any optional parameters that are needed.
*
* @param string $portion Part of the URI specification
* @param bool $required Whether or not parameters are required (initially)
* @return array Tuple of the compiled portion and whether or not it contained specified parameters
* @todo anon function to method
*/
$compile = function ($portion, $required) use (&$compile, $defaults, $params) {
$missing = [];
$pattern = '#(?:'.self::REGEX_KEY.'|'.self::REGEX_GROUP.')#';
$result = preg_replace_callback(
$pattern,
function ($matches) use (&$compile, $defaults, &$missing, $params, &$required) {
if ($matches[0][0] == '<') {
// Parameter, unwrapped
$param = $matches[1];
if (isset($params[$param])) {
// This portion is required when a specified
// parameter does not match the default
$required = $required
|| !isset($defaults[$param])
|| $params[$param] !== $defaults[$param];
// Add specified parameter to this result
return $params[$param];
}
// Add default parameter to this result
if (isset($defaults[$param])) {
return $defaults[$param];
}
// This portion is missing a parameter
$missing[] = $param;
} else {
// Group, unwrapped
$result = $compile($matches[2], false);
if ($result[1]) {
// This portion is required when it contains a group that is required
$required = true;
// Add required groups to this result
return $result[0];
}
// Note! Don't add optional groups to this result
}
},
$portion
);
if ($required && $missing) {
// @todo Message as object
throw new Exception(sprintf(
'Required route %s parameter not passed: %s',
$this->name,
reset($missing)
));
}
return array($result, $required);
};
list($uri) = $compile($this->uri, true);
// Trim all extra slashes from the URI
//$uri = preg_replace('#//+#', '/', rtrim($uri, '/'));
$uri = preg_replace('#//+#', '/', $uri);
/* @todo
if ($this->defaults['host']) {
// Need to add the host to the URI
$host = $this->defaults['host'];
if (strpos($host, '://') === false) {
// Use the default defined protocol
if (empty($protocol) {
$protocol = $this->defaults['protocol'];
}
$host = $protocol.'://'.$host;
}
// Clean up the host and prepend it to the URI
$uri = rtrim($host, '/').'/'. $uri;
}
*/
return $uri;
}
}
<?php
namespace Enso\Core;
/**
* Routes are used to determine the controller and action for a requested URI.
* Every route generates a regular expression which is used to match a URI
* and a route. Routes may also contain keys which can be used to set the
* controller, action, and parameters.
*
* @package Enso\Core
* @copyright (c) 2016-2018 Enso team
* @license MIT License
*/
class Router
{
/**
* @var string Default protocol
*/
protected $protocol = 'http';
/**
* @var array List of valid localhost entries
*/
protected $localhosts = [
false,
'',
'localhost'
];
/**
* @var array List of routes
*/
protected $routes = [];
/**
* @param string $protocol Default protocol
* @param array $localhosts List of valid localhost entries
* @return void
*/
public function __construct($protocol = '', array $localhosts = [])
{
if ($protocol) {
$this->setProtocol($protocol);
}
if ($localhosts) {
$this->localhosts = $localhosts;
}
}
/**
* @param string $protocol
* @return Router
*/
public function setProtocol($protocol)
{
$this->protocol = rtrim($protocol, '://');
return $this;
}
/**
* @return string
*/
public function getProtocol()
{
return $this->protocol;
}
/**
* @param array $localhosts
* @param bool $append
* @return Router
*/
public function setLocalhosts(array $localhosts, $append = false)
{
if ($append) {
$localhosts = array_merge($this->localhosts, $localhosts);
}
$this->localhosts = $localhosts;
return $this;
}
/**
* @return array
*/
public function getLocalhosts()
{
return $this->localhosts;
}
/**
* Stores a named route and returns it. The "action" will always
* be set to "index" if it is not defined.
*
* @param Route $route Route instance
* @param bool $prepend Prepend or append route?
* @return Router
*/
public function setRoute(Route $route, $prepend = false)
{
$name = $route->getName();
if ($prepend) {
$this->routes = [$name => $route] + $this->routes;
} else {
$this->routes[$name] = $route;
}
return $this;
}
/**
* @param string|Route $name Route name
* @return Route
*/
public function getRoute($name)
{
return $this->routes[strval($name)];
}
/**
* @return array
*/
public function getAllRoutes()
{
return $this->routes;
}
/**
* @param string|Route $name Route name
* @return bool
*/
public function routeExists($name)
{
return isset($this->routes[strval($name)]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.