Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
PHP classes of Enso\Router
<?php declare(strict_types=1);
namespace Enso\Core;
use InvalidArgumentException;
/**
* Routes are used to determine the namespace, 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 namespace, controller, action and parameters.
*
* @package Enso\Core
* @copyright (c) 2016-2019 Enso team
* @license MIT
*/
class Route
{
// What must be escaped in the route regex
const ESCAPE = '.\+*?[^]${}=!|:-#';
// @todo const REGEX_ESCAPE = '#[-.\\+*?[^\]${}=!|]#u';
// What can be part of a `<segment>` value
const SEGMENT = '[^/.,;?\n]++';
// Defines the pattern of a `<segment>`
const PARAM = '<([a-zA-Z0-9_]++)>';
// Matches a URI group and captures the contents
const GROUP = '\(((?:(?>[^()]+)|(?R))*)\)';
// Part pattern
const PORTION = '#(?:' . self::PARAM . '|' . self::GROUP . ')#u';
/**
* @var string Identifier
*/
protected $name;
/**
* @var string URI pattern
*/
protected $uri;
/**
* @var string Compiled URI
*/
protected $compiledUri;
/**
* @var array Keys patterns uses in URI pattern
*/
protected $keys = [
'protocol' => 'http|https',
'host' => '[-_.a-z0-9]+',
'namespace' => '[a-zA-Z0-9\\]+',
'controller' => '[a-zA-Z0-9]+',
'action' => '[_a-zA-Z0-9]+',
];
/**
* @var array Default parameter values
*/
protected $defaults = [
'protocol' => 'http',
'host' => '',
'namespace' => 'Controller',
'controller' => '',
'action' => 'index',
];
/**
* @var callable Callback filtering parameters
*/
protected $filter;
/**
* Returns route name.
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return string
*/
public function __toString()
{
return $this->getName();
}
/**
* Returns key values by default.
* @return array
*/
public function getDefaults()
{
return $this->defaults;
}
/**
* Gets parameters filter.
* @return callable|null
*/
public function getFilter()
{
return $this->filter;
}
/**
* Sets parameters filter.
* @param callable $callback Filter
* @return self
*/
public function setFilter(callable $callback)
{
$this->filter = $callback;
return $this;
}
/**
* Sets parameter presets.
* @param array $params Pairs: 'parameter' => 'default value' or ['default value', 'value`s pattern']
* @return self
*/
public function setParams(array $params)
{
foreach ($params as $key => $value) {
if (! is_array($value)) {
$value = [$value];
}
if (count($value) == 1) {
list($this->defaults[$key]) = $value;
} else {
list($this->defaults[$key], $this->keys[$key]) = $value;
}
}
// For recompile URI pattern
unset($this->compiledUri);
return $this;
}
/**
* Creates a route instance.
* @param string $name Identifier
* @param string $uri URI pattern
* @param array $params Parameter presets
* @param callable $filter Callback filtering parameters
* @return void
*/
public function __construct(string $name, string $uri, array $params = [], callable $filter = NULL)
{
$this->name = strtolower($name);
$this->uri = trim($uri);
if ($params) {
$this->setParams($params);
}
if ($filter) {
$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 (! $this->compiledUri) {
// The URI should be considered literal except for keys and optional parts.
// Escape everything `preg_quote()` would escape except for: '(', ')', '<', '>'.
// @todo $uri = preg_replace(self::ESCAPE, '\\\\$0', $this->uri);
$uri = addcslashes($this->uri, self::ESCAPE);
if (strpos(')')) {
// Make optional parts of the URI non-capturing and optional.
$uri = strtr($uri, ['(' => '(?:', ')' => ')?']);
}
// Insert default regex for keys.
$uri = strtr($uri, ['<' => '(?P<', '>' => '>' . self::SEGMENT . ')']);
if ($this->keys) {
$replaces = [];
foreach ($this->keys as $key => $value) {
$replaces['<' . $key . '>' . self::SEGMENT] = '<' . $key . '>' . $value;
}
// Replace the default expression with the user-specified expression.
$uri = strtr($uri, $replaces);
}
// Store the compiled regex locally
$this->compiledUri = '#^' . $uri . '$#uD';
}
return $this->compiledUri;
}
/**
*
* @param array $params
* @return array
*/
protected function normalizeParams(array $params)
{
if (! empty($params['protocol'])) {
$params['protocol'] = strtolower($params['protocol']);
}
if (! empty($params['namespace'])) {
$params['namespace'] = str_replace(['/', '-', '_'], '\\', $params['namespace']);
$params['namespace'] = ucwords(rtrim($params['namespace'], '\\'), '\\');
}
if (! empty($params['controller'])) {
$params['controller'] = ucfirst($params['controller']);
}
if (! empty($params['action'])) {
$params['action'] = strtr(ucwords($params['action'], '_'), ['_' => '']);
}
return $params;
}
/**
* Tests if the route matches a given request, returns routed parameters at success or `FALSE` at fail.
* @param Request $request Request instance
* @return array|bool
*/
public function matches(Request $request)
{
if (! preg_match($this->getCompiledUri(), $request->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;
}
}
$params = $this->normalizeParams($params);
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
* @return string
* @throws InvalidArgumentException
*/
public function getUri(array $params = [])
{
if ($params) {
$params = array_map('rawurlencode', $params);
// Decode slashes back, see Apache docs about AllowEncodedSlashes and AcceptPathInfo
$params = str_replace(['%2F', '%5C'], ['/', '\\'], $params);
}
/**
* 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 replace closures to methods
*/
$compiler = function ($portion, $required) use (& $compiler, $params)
{
$missing = [];
$callback = function ($matches) use (& $compiler, & $missing, & $required, $params)
{
if ($matches[0][0] == '<') {
// Unwrapped parameter
$param = $matches[1];
if (isset($params[$param])) {
// This portion is required when a specified parameter does not match the default.
if (! $required) {
$required = ! isset($this->defaults[$param]) || $params[$param] !== $this->defaults[$param];
}
// Add specified parameter to this result
return $params[$param];
}
// Add default parameter to this result
if (isset($this->defaults[$param])) {
return $this->defaults[$param];
}
// This portion is missing a parameter
$missing[] = $param;
} else {
// Unwrapped group
$result = $compiler($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!
}
};
$result = preg_replace_callback(self::PORTION, $callback, $portion);
if ($required && $missing) {
throw new InvalidArgumentException(sprintf(
'Route %s: required parameters(%s) not passed.',
$this->name,
implode(', ', $missing)
));
}
return [$result, $required];
};
list($uri) = $compiler($this->uri, TRUE);
// Trim all extra slashes from the URI
return preg_replace('#//+#u', '/', $uri);
}
}
<?php declare(strict_types=1);
namespace Enso\Core;
use UnexpectedValueException;
/**
* Routes are used to determine the namespace, controller and action for a requested URI. Every route generates
* a regularexpression which is used to match a URI and a route. Routes may also contain keys which can be used
* to set the namespace, controller, action and parameters.
*
* @package Enso\Core
* @copyright (c) 2016-2019 Enso team
* @license MIT
*/
class Router
{
/**
* @var array Stored routes
*/
protected $routes = [];
/**
* Stores a named route.
* @param Route $route `Route` instance
* @param bool $prepend Prepend route?
* @return self
*/
public function addRoute(Route $route, $prepend = FALSE)
{
if ($prepend) {
$this->routes = [$route->getName() => $route] + $this->routes;
} else {
$this->routes[$route->getName()] = $route;
}
return $this;
}
/**
* Returns route or `false` if route not exists.
* @param string|Route $name Route name or route instance
* @return Route|bool
*/
public function getRoute($name)
{
$name = (string) $name;
return isset($this->routes[$name]) ? $this->routes[$name] : FALSE;
}
/**
* Deletes stored route.
* @param string|Route $name Route name or route instance
* @return self
*/
public function deleteRoute($name)
{
unset($this->routes[strval($name)]);
return $this;
}
/**
* Returns all routes.
* @return array
*/
public function getRoutes()
{
return $this->routes;
}
/**
* Returns request`s route with dispatched parameters.
* @param Request $request Request instance
* @return array ['route', 'params']
* @throws UnexpectedValueException
*/
public function dispatch(Request $request)
{
foreach ($this->routes as $route) {
$params = $route->matches($request);
if (is_array($params)) {
return compact('route', 'params');
}
}
throw new UnexpectedValueException('Request ' . $request->uri() . ': route not detected.');
}
}
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.