Files
simple-php-router/src/Pecee/SimpleRouter/Route/Route.php
Simon Sessingø ca88e86c3d Development
- Removed unused exception from PHP-docs.
- Fixed types not same as declared.
- Fixed issues with reg-ex and php-unit tests.
- Removed unnecessary type casting.
- Declared functions as static (better scoping + performance).
- Moved `\is_callable($callback) === false` as the execution costs less than previous in `Router.php`.
- Changed `ob_get_contents` to `ob_get_clean`.
- Added type hints to methods.
2021-03-17 20:20:28 +01:00

578 lines
14 KiB
PHP

<?php
namespace Pecee\SimpleRouter\Route;
use Pecee\Http\Middleware\IMiddleware;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Exceptions\NotFoundHttpException;
use Pecee\SimpleRouter\Router;
abstract class Route implements IRoute
{
protected const PARAMETERS_REGEX_FORMAT = '%s([\w]+)(\%s?)%s';
protected const PARAMETERS_DEFAULT_REGEX = '[\w]+';
public const REQUEST_TYPE_GET = 'get';
public const REQUEST_TYPE_POST = 'post';
public const REQUEST_TYPE_PUT = 'put';
public const REQUEST_TYPE_PATCH = 'patch';
public const REQUEST_TYPE_OPTIONS = 'options';
public const REQUEST_TYPE_DELETE = 'delete';
public const REQUEST_TYPE_HEAD = 'head';
public static $requestTypes = [
self::REQUEST_TYPE_GET,
self::REQUEST_TYPE_POST,
self::REQUEST_TYPE_PUT,
self::REQUEST_TYPE_PATCH,
self::REQUEST_TYPE_OPTIONS,
self::REQUEST_TYPE_DELETE,
self::REQUEST_TYPE_HEAD,
];
/**
* If enabled parameters containing null-value
* will not be passed along to the callback.
*
* @var bool
*/
protected $filterEmptyParams = true;
/**
* Default regular expression used for parsing parameters.
* @var string|null
*/
protected $defaultParameterRegex;
protected $paramModifiers = '{}';
protected $paramOptionalSymbol = '?';
protected $urlRegex = '/^%s\/?$/u';
protected $group;
protected $parent;
protected $callback;
protected $defaultNamespace;
/* Default options */
protected $namespace;
protected $requestMethods = [];
protected $where = [];
protected $parameters = [];
protected $originalParameters = [];
protected $middlewares = [];
/**
* Render route
*
* @param Request $request
* @param Router $router
* @return string|null
* @throws NotFoundHttpException
*/
public function renderRoute(Request $request, Router $router): ?string
{
$router->debug('Starting rendering route "%s"', \get_class($this));
$callback = $this->getCallback();
if ($callback === null) {
return null;
}
$router->debug('Parsing parameters');
$parameters = $this->getParameters();
$router->debug('Finished parsing parameters');
/* Filter parameters with null-value */
if ($this->filterEmptyParams === true) {
$parameters = array_filter($parameters, static function ($var) {
return ($var !== null);
});
}
/* Render callback function */
if (\is_callable($callback) === true) {
$router->debug('Executing callback');
/* When the callback is a function */
return $router->getClassLoader()->loadClosure($callback, $parameters);
}
/* When the callback is a class + method */
$controller = explode('@', $callback);
$namespace = $this->getNamespace();
$className = ($namespace !== null && $controller[0][0] !== '\\') ? $namespace . '\\' . $controller[0] : $controller[0];
$router->debug('Loading class %s', $className);
$class = $router->getClassLoader()->loadClass($className);
if (\count($controller) === 1) {
$controller[1] = '__invoke';
}
$method = $controller[1];
if (method_exists($class, $method) === false) {
throw new NotFoundHttpException(sprintf('Method "%s" does not exist in class "%s"', $method, $className), 404);
}
$router->debug('Executing callback');
return \call_user_func_array([$class, $method], $parameters);
}
protected function parseParameters($route, $url, $parameterRegex = null): ?array
{
$regex = (strpos($route, $this->paramModifiers[0]) === false) ? null :
sprintf
(
static::PARAMETERS_REGEX_FORMAT,
$this->paramModifiers[0],
$this->paramOptionalSymbol,
$this->paramModifiers[1]
);
// Ensures that host names/domains will work with parameters
$url = '/' . ltrim($url, '/');
$urlRegex = '';
$parameters = [];
if ($regex === null || (bool)preg_match_all('/' . $regex . '/u', $route, $parameters) === false) {
$urlRegex = preg_quote($route, '/');
} else {
foreach (preg_split('/((\-?\/?)\{[^}]+\})/', $route) as $key => $t) {
$regex = '';
if ($key < \count($parameters[1])) {
$name = $parameters[1][$key];
/* If custom regex is defined, use that */
if (isset($this->where[$name]) === true) {
$regex = $this->where[$name];
} else {
$regex = $parameterRegex ?? $this->defaultParameterRegex ?? static::PARAMETERS_DEFAULT_REGEX;
}
$regex = sprintf('((\/|\-)(?P<%2$s>%3$s))%1$s', $parameters[2][$key], $name, $regex);
}
$urlRegex .= preg_quote($t, '/') . $regex;
}
}
if (trim($urlRegex) === '' || (bool)preg_match(sprintf($this->urlRegex, $urlRegex), $url, $matches) === false) {
return null;
}
$values = [];
if (isset($parameters[1]) === true) {
/* Only take matched parameters with name */
foreach ((array)$parameters[1] as $name) {
$values[$name] = (isset($matches[$name]) === true && $matches[$name] !== '') ? $matches[$name] : null;
}
}
return $values;
}
/**
* Returns callback name/identifier for the current route based on the callback.
* Useful if you need to get a unique identifier for the loaded route, for instance
* when using translations etc.
*
* @return string
*/
public function getIdentifier(): string
{
if (\is_string($this->callback) === true && strpos($this->callback, '@') !== false) {
return $this->callback;
}
return 'function:' . md5($this->callback);
}
/**
* Set allowed request methods
*
* @param array $methods
* @return static
*/
public function setRequestMethods(array $methods): IRoute
{
$this->requestMethods = $methods;
return $this;
}
/**
* Get allowed request methods
*
* @return array
*/
public function getRequestMethods(): array
{
return $this->requestMethods;
}
/**
* @return IRoute|null
*/
public function getParent(): ?IRoute
{
return $this->parent;
}
/**
* Get the group for the route.
*
* @return IGroupRoute|null
*/
public function getGroup(): ?IGroupRoute
{
return $this->group;
}
/**
* Set group
*
* @param IGroupRoute $group
* @return static
*/
public function setGroup(IGroupRoute $group): IRoute
{
$this->group = $group;
/* Add/merge parent settings with child */
return $this->setSettings($group->toArray(), true);
}
/**
* Set parent route
*
* @param IRoute $parent
* @return static
*/
public function setParent(IRoute $parent): IRoute
{
$this->parent = $parent;
return $this;
}
/**
* Set callback
*
* @param string $callback
* @return static
*/
public function setCallback($callback): IRoute
{
$this->callback = $callback;
return $this;
}
/**
* @return string|callable
*/
public function getCallback()
{
return $this->callback;
}
public function getMethod(): ?string
{
if (\is_string($this->callback) === true && strpos($this->callback, '@') !== false) {
$tmp = explode('@', $this->callback);
return $tmp[1];
}
return null;
}
public function getClass(): ?string
{
if (\is_string($this->callback) === true && strpos($this->callback, '@') !== false) {
$tmp = explode('@', $this->callback);
return $tmp[0];
}
return null;
}
public function setMethod(string $method): IRoute
{
$this->callback = sprintf('%s@%s', $this->getClass(), $method);
return $this;
}
public function setClass(string $class): IRoute
{
$this->callback = sprintf('%s@%s', $class, $this->getMethod());
return $this;
}
/**
* @param string $namespace
* @return static
*/
public function setNamespace(string $namespace): IRoute
{
$this->namespace = $namespace;
return $this;
}
/**
* @param string $namespace
* @return static
*/
public function setDefaultNamespace($namespace): IRoute
{
$this->defaultNamespace = $namespace;
return $this;
}
public function getDefaultNamespace(): ?string
{
return $this->defaultNamespace;
}
/**
* @return string|null
*/
public function getNamespace(): ?string
{
return $this->namespace ?? $this->defaultNamespace;
}
/**
* Export route settings to array so they can be merged with another route.
*
* @return array
*/
public function toArray(): array
{
$values = [];
if ($this->namespace !== null) {
$values['namespace'] = $this->namespace;
}
if (\count($this->requestMethods) !== 0) {
$values['method'] = $this->requestMethods;
}
if (\count($this->where) !== 0) {
$values['where'] = $this->where;
}
if (\count($this->middlewares) !== 0) {
$values['middleware'] = $this->middlewares;
}
if ($this->defaultParameterRegex !== null) {
$values['defaultParameterRegex'] = $this->defaultParameterRegex;
}
return $values;
}
/**
* Merge with information from another route.
*
* @param array $values
* @param bool $merge
* @return static
*/
public function setSettings(array $values, bool $merge = false): IRoute
{
if ($this->namespace === null && isset($values['namespace']) === true) {
$this->setNamespace($values['namespace']);
}
if (isset($values['method']) === true) {
$this->setRequestMethods(array_merge($this->requestMethods, (array)$values['method']));
}
if (isset($values['where']) === true) {
$this->setWhere(array_merge($this->where, (array)$values['where']));
}
if (isset($values['parameters']) === true) {
$this->setParameters(array_merge($this->parameters, (array)$values['parameters']));
}
// Push middleware if multiple
if (isset($values['middleware']) === true) {
$this->setMiddlewares(array_merge((array)$values['middleware'], $this->middlewares));
}
if (isset($values['defaultParameterRegex']) === true) {
$this->setDefaultParameterRegex($values['defaultParameterRegex']);
}
return $this;
}
/**
* Get parameter names.
*
* @return array
*/
public function getWhere(): array
{
return $this->where;
}
/**
* Set parameter names.
*
* @param array $options
* @return static
*/
public function setWhere(array $options): IRoute
{
$this->where = $options;
return $this;
}
/**
* Add regular expression parameter match.
* Alias for LoadableRoute::where()
*
* @see LoadableRoute::where()
* @param array $options
* @return static
*/
public function where(array $options)
{
return $this->setWhere($options);
}
/**
* Get parameters
*
* @return array
*/
public function getParameters(): array
{
/* Sort the parameters after the user-defined param order, if any */
$parameters = [];
if (\count($this->originalParameters) !== 0) {
$parameters = $this->originalParameters;
}
return array_merge($parameters, $this->parameters);
}
/**
* Get parameters
*
* @param array $parameters
* @return static
*/
public function setParameters(array $parameters): IRoute
{
/*
* If this is the first time setting parameters we store them so we
* later can organize the array, in case somebody tried to sort the array.
*/
if (\count($parameters) !== 0 && \count($this->originalParameters) === 0) {
$this->originalParameters = $parameters;
}
$this->parameters = array_merge($this->parameters, $parameters);
return $this;
}
/**
* Add middleware class-name
*
* @deprecated This method is deprecated and will be removed in the near future.
* @param IMiddleware|string $middleware
* @return static
*/
public function setMiddleware($middleware)
{
$this->middlewares[] = $middleware;
return $this;
}
/**
* Add middleware class-name
*
* @param IMiddleware|string $middleware
* @return static
*/
public function addMiddleware($middleware): IRoute
{
$this->middlewares[] = $middleware;
return $this;
}
/**
* Set middlewares array
*
* @param array $middlewares
* @return static
*/
public function setMiddlewares(array $middlewares): IRoute
{
$this->middlewares = $middlewares;
return $this;
}
/**
* @return array
*/
public function getMiddlewares(): array
{
return $this->middlewares;
}
/**
* Set default regular expression used when matching parameters.
* This is used when no custom parameter regex is found.
*
* @param string $regex
* @return static
*/
public function setDefaultParameterRegex($regex)
{
$this->defaultParameterRegex = $regex;
return $this;
}
/**
* Get default regular expression used when matching parameters.
*
* @return string
*/
public function getDefaultParameterRegex(): string
{
return $this->defaultParameterRegex;
}
}