-
-
Save Golpha/1db1443f7a3decd5f444 to your computer and use it in GitHub Desktop.
SPC Proposal Implementation of Route and Router
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* StandardPHPComponents-Proposal | |
* Developer: tr0y | |
* Date: 21.02.14 | |
*/ | |
class SpcRoute { | |
/** | |
* Accepted request methods of this route | |
* @var array | |
*/ | |
protected $requestMethods = array(); | |
/** | |
* generated route data of delivered route path | |
* @var array | |
*/ | |
protected $routeData = array(); | |
/** | |
* aggregated regular expression for this route | |
* @var | |
*/ | |
protected $regex; | |
/** | |
* userland vars inside of the route path | |
* @var | |
*/ | |
protected $vars; | |
/** | |
* Constructor | |
* triggers the creation of userland vars, route data and regular expression of this route. | |
* | |
* @param $path | |
*/ | |
public function __construct($path) | |
{ | |
$this->_fetchRouteData($path); | |
list($this->regex, $this->vars) = $this->_buildMatchCredentials(); | |
if ( empty($this->routeData) ) { | |
$this->regex = $path; | |
$this->routeData = array($path); | |
} | |
} | |
/** | |
* useMethod() | |
* attaches a http verb to the current stack of used request methods for this route. | |
* | |
* @param $httpVerb | |
*/ | |
public function useMethod($httpVerb) | |
{ | |
$this->requestMethods[] = strtoupper($httpVerb); | |
$this->requestMethods = array_unique($this->requestMethods); | |
} | |
/** | |
* matches() | |
* Query method to determine if the current route matches against a string. | |
* | |
* @param $pathInfo | |
* @return bool | |
* @throws LogicException | |
*/ | |
public function matches($pathInfo) | |
{ | |
if ( empty($this->requestMethods) ) { | |
throw new LogicException('no method attached'); | |
} | |
return (boolean) preg_match('~^'.$this->regex.'$~', $pathInfo); | |
} | |
/** | |
* getData() | |
* Query method for the data of the url. This method aggregates the userland variables to | |
* a array of their assigned url-data. | |
* | |
* @param $pathInfo | |
* @return array | |
*/ | |
public function getData($pathInfo) | |
{ | |
if ( $this->matches($pathInfo) ) { | |
preg_match('~^(?:'.$this->regex.')$~', $pathInfo, $matches); | |
return array_combine($this->vars, array_slice($matches, 1)); | |
} | |
return array(); | |
} | |
public function getRouteEntites() | |
{ | |
return $this->routeData; | |
} | |
public function getRequestMethods() | |
{ | |
return $this->requestMethods; | |
} | |
/** | |
* ::create() | |
* Factory method of this route. | |
* | |
* @param $path | |
* @param array $methods | |
* @return static | |
*/ | |
public static function create($path, array $methods) | |
{ | |
$route = new static($path); | |
foreach ( $methods as $verb ) { | |
$route->useMethod($verb); | |
} | |
return $route; | |
} | |
/** | |
* _fetchRouteData() | |
* Internal parse-method for the route path. | |
* | |
* This is a clone of the fast route mechanism developed by Nikita Popov. | |
* | |
* @param $path | |
* @return array | |
*/ | |
private function _fetchRouteData($path) | |
{ | |
$varRegex = '~\{\s*([a-zA-Z][a-zA-Z0-9_]*)\s*(?::\s*([^{}]*(?:\{(?-1)\}[^{}*])*))?\}~x'; | |
$defaultRegex = '[^/]+'; | |
if ( ! preg_match_all($varRegex, $path, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER) ) { | |
return array($path); | |
} | |
$offset = 0; | |
$routeData = array(); | |
foreach ( $matches as $set ) { | |
if ( $set[0][1] > $offset ) { | |
$routeData[] = substr($path, $offset, $set[0][1] - $offset); | |
} | |
$routeData[] = array( | |
$set[1][0], | |
isset($set[2]) | |
? trim($set[2][0]) | |
: $defaultRegex | |
); | |
$offset = $set[0][1] + strlen($set[0][0]); | |
} | |
$this->routeData = $routeData; | |
} | |
/** | |
* _buildMatchCredentials() | |
* Internal userland variables url string extraction method. | |
* | |
* This is a clone of the fast route mechanism developed by Nikita Popov. | |
* | |
* @return array | |
* @throws LogicException | |
*/ | |
private function _buildMatchCredentials() | |
{ | |
$regex = ''; | |
$vars = array(); | |
foreach ( $this->routeData as $current ) { | |
if ( is_string($current) ) { | |
$regex .= preg_quote($current, '~'); | |
continue; | |
} | |
list($name, $currentRegex) = $current; | |
if ( isset($vars[$name]) ) { | |
throw new LogicException( | |
'Impossible to use placeholder "'.$name.'" more then once' | |
); | |
} | |
$vars[$name] = $name; | |
$regex .= '('.$currentRegex.')'; | |
} | |
return array($regex, $vars); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* StandardPHPComponents-Proposal | |
* Developer: tr0y | |
* Date: 21.02.14 | |
*/ | |
class SpcRouter | |
{ | |
/** | |
* The registered routes | |
* @var array | |
*/ | |
protected $routes = array(); | |
/** | |
* the callback injector | |
* @var | |
*/ | |
protected $callbackInjector; | |
/** | |
* registerRoute() | |
* registers a SpcRoute-Object as a legal route. | |
* | |
* @param $name | |
* @param SpcRoute $route | |
* @param null $callback | |
* @throws LogicException | |
*/ | |
public function registerRoute($name, SpcRoute $route, $callback = null) | |
{ | |
if ( isset($this->routes[$name]) ) { | |
throw new LogicException('Route with name "'.$name.'" already registered'); | |
} | |
$preparedEntities = array_map(function($current) { | |
return is_array($current) ? '*' : strtolower($current); | |
}, $route->getRouteEntites()); | |
$routeHash = sha1(implode('', $preparedEntities)); | |
foreach ( $this->routes as $currentName => $currentRoute ) { | |
if ( $currentRoute['hash'] === $routeHash ) { | |
throw new LogicException('Route with same behavior is already registered as: '.$currentName); | |
} | |
} | |
$this->routes[$name] = array( | |
'hash' => $routeHash, | |
'route' => $route, | |
'callback' => $callback, | |
); | |
} | |
/** | |
* registerPath() | |
* aggregates a SpcRoute-Object with the given parameters and calls registerRoute(). | |
* | |
* @param $name | |
* @param $path | |
* @param $methods | |
* @param $callback | |
*/ | |
public function registerPath($name, $path, $methods, $callback) | |
{ | |
$methods = (array) $methods; | |
$this->registerRoute($name, SpcRoute::create($path, $methods), $callback); | |
} | |
/** | |
* setCallbackInjector() | |
* Setter for injector callback. The injector will be called before calling the route callback to inject | |
* dependencies. The injector callback MUST return an array of the dependencies in the same order the | |
* route callback method-/function- signature has defined them. | |
* | |
* @param $callback | |
*/ | |
public function setCallbackInjector($callback) | |
{ | |
if ( ! is_callable($callback) ) { | |
$this->callbackInjector = $callback; | |
} | |
} | |
/** | |
* getCurrentRoute() | |
* fetches the current route ( if there was one ) | |
* | |
* @param SpcRequest $request | |
* @throws SpcBadMethodException | |
* @return bool|int|string | |
*/ | |
public function getCurrentRoute(SpcRequest $request = null) | |
{ | |
if ( $request instanceof SpcRequest ) { | |
$requestMethod = $request->getRequestMethod(); | |
if ( ! $request->hasServerValue('PATH_INFO') ) { | |
$pathInfo = $request->getServerValue('PATH_INFO'); | |
} | |
else { | |
$pathInfo = '/'; | |
} | |
} | |
else { | |
$requestMethod = filter_input(INPUT_SERVER, 'REQUEST_METHOD'); | |
if ( filter_has_var(INPUT_SERVER, 'PATH_INFO') ) { | |
$pathInfo = filter_input(INPUT_SERVER, 'PATH_INFO'); | |
} | |
else { | |
$pathInfo = '/'; | |
} | |
} | |
foreach ( $this->routes as $routeName => $routeArray ) { | |
if ( $routeArray['route']->matches($pathInfo) ) { | |
if ( ! in_array($requestMethod, $routeArray['route']->getRequestMethods()) ) { | |
throw new SpcBadMethodException('A path has been resolved to a route, but the method is not allowed'); | |
} | |
return $routeName; | |
} | |
} | |
return false; | |
} | |
/** | |
* executeCurrentRoute() | |
* executes the current route. The fallbackCallback could be used to catch thrown SpcRouterException-Exceptions. | |
* | |
* @param SpcRequest $request | |
* @param callback|null $fallbackCallback | |
* @return mixed | |
* @throws LogicException | |
* @throws SpcRouteNotFoundException | |
*/ | |
public function executeCurrentRoute($fallbackCallback = null, SpcRequest $request = null) | |
{ | |
try { | |
$currentRoute = $this->getCurrentRoute($request); | |
} | |
catch ( SpcRouterException $exception ) { | |
if ( null === $fallbackCallback ) { | |
throw $exception; | |
} | |
return call_user_func($fallbackCallback, $exception); | |
} | |
if ( ! $currentRoute ) { | |
$exception = new SpcRouteNotFoundException('No route found'); | |
if ( null !== $fallbackCallback ) { | |
return call_user_func($fallbackCallback, $exception); | |
} | |
throw $exception; | |
} | |
if ( null === $this->routes[$currentRoute]['callback'] ) { | |
throw new LogicException('There was a route, but no callback to execute'); | |
} | |
if ( null !== $this->callbackInjector ) { | |
$incubatedDependencies = call_user_func( | |
$this->callbackInjector, | |
$this->routes[$currentRoute]['callback'] | |
); | |
if ( ! is_array($incubatedDependencies) ) { | |
throw new LogicException('The callback injector must return an array'); | |
} | |
if ( $request instanceof SpcRequest ) { | |
if ( ! $request->hasServerValue('PATH_INFO') ) { | |
$pathInfo = $request->getServerValue('PATH_INFO'); | |
} | |
else { | |
$pathInfo = '/'; | |
} | |
} | |
else { | |
if ( filter_has_var(INPUT_SERVER, 'PATH_INFO') ) { | |
$pathInfo = filter_input(INPUT_SERVER, 'PATH_INFO'); | |
} | |
else { | |
$pathInfo = '/'; | |
} | |
} | |
if ( count($data = $this->routes[$currentRoute]['route']->getData($pathInfo)) > 0 ) { | |
array_unshift($incubatedDependencies, $data); | |
} | |
return call_user_func_array( | |
$this->routes[$currentRoute]['callback'], | |
$incubatedDependencies | |
); | |
} | |
else { | |
if ( $request instanceof SpcRequest ) { | |
if ( ! $request->hasServerValue('PATH_INFO') ) { | |
$pathInfo = $request->getServerValue('PATH_INFO'); | |
} | |
else { | |
$pathInfo = '/'; | |
} | |
} | |
else { | |
if ( filter_has_var(INPUT_SERVER, 'PATH_INFO') ) { | |
$pathInfo = filter_input(INPUT_SERVER, 'PATH_INFO'); | |
} | |
else { | |
$pathInfo = '/'; | |
} | |
} | |
if ( count($data = $this->routes[$currentRoute]['route']->getData($pathInfo)) > 0 ) { | |
return call_user_func($this->routes[$currentRoute]['callback'], $data); | |
} | |
return call_user_func($this->routes[$currentRoute]['callback']); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment