Skip to content

Instantly share code, notes, and snippets.

@Golpha
Created February 21, 2014 11:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Golpha/1db1443f7a3decd5f444 to your computer and use it in GitHub Desktop.
Save Golpha/1db1443f7a3decd5f444 to your computer and use it in GitHub Desktop.
SPC Proposal Implementation of Route and Router
<?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);
}
}
<?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