Skip to content

Instantly share code, notes, and snippets.

Created October 27, 2014 16:02
Show Gist options
  • Select an option

  • Save anonymous/aa4501cd4731c7ce60b4 to your computer and use it in GitHub Desktop.

Select an option

Save anonymous/aa4501cd4731c7ce60b4 to your computer and use it in GitHub Desktop.

Revisions

  1. @invalid-email-address Anonymous created this gist Oct 27, 2014.
    273 changes: 273 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,273 @@
    <?php

    class Router {

    protected $routes = array();
    protected $namedRoutes = array();
    protected $basePath = '';
    protected $matchTypes = array(
    'i' => '[0-9]++',
    'a' => '[0-9A-Za-z]++',
    'h' => '[0-9A-Fa-f]++',
    '*' => '.+?',
    '**' => '.++',
    '' => '[^/\.]++'
    );

    /**
    * Create router in one call from config.
    *
    * @param array $routes
    * @param string $basePath
    * @param array $matchTypes
    */
    public function __construct( $routes = array(), $basePath = '', $matchTypes = array() ) {
    $this->addRoutes($routes);
    $this->setBasePath($basePath);
    $this->addMatchTypes($matchTypes);
    }

    /**
    * Add multiple routes at once from array in the following format:
    *
    * $routes = array(
    * array($method, $route, $target, $name)
    * );
    *
    * @param array $routes
    * @return void
    * @author Koen Punt
    */
    public function addRoutes($routes){
    if(!is_array($routes) && !$routes instanceof Traversable) {
    throw new \Exception('Routes should be an array or an instance of Traversable');
    }
    foreach($routes as $route) {
    call_user_func_array(array($this, 'map'), $route);
    }
    }

    /**
    * Set the base path.
    * Useful if you are running your application from a subdirectory.
    */
    public function setBasePath($basePath) {
    $this->basePath = $basePath;
    }

    /**
    * Add named match types. It uses array_merge so keys can be overwritten.
    *
    * @param array $matchTypes The key is the name and the value is the regex.
    */
    public function addMatchTypes($matchTypes) {
    $this->matchTypes = array_merge($this->matchTypes, $matchTypes);
    }

    /**
    * Map a route to a target
    *
    * @param string $method One of 5 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|PATCH|PUT|DELETE)
    * @param string $route The route regex, custom regex must start with an @. You can use multiple pre-set regex filters, like [i:id]
    * @param mixed $target The target where this route should point to. Can be anything.
    * @param string $name Optional name of this route. Supply if you want to reverse route this url in your application.
    */
    public function map($method, $route, $target, $name = null) {

    $this->routes[] = array($method, $route, $target, $name);

    if($name) {
    if(isset($this->namedRoutes[$name])) {
    throw new \Exception("Can not redeclare route '{$name}'");
    } else {
    $this->namedRoutes[$name] = $route;
    }

    }

    return;
    }

    /**
    * Reversed routing
    *
    * Generate the URL for a named route. Replace regexes with supplied parameters
    *
    * @param string $routeName The name of the route.
    * @param array @params Associative array of parameters to replace placeholders with.
    * @return string The URL of the route with named parameters in place.
    */
    public function generate($routeName, array $params = array()) {

    // Check if named route exists
    if(!isset($this->namedRoutes[$routeName])) {
    throw new \Exception("Route '{$routeName}' does not exist.");
    }

    // Replace named parameters
    $route = $this->namedRoutes[$routeName];

    // prepend base path to route url again
    $url = $this->basePath . $route;

    if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) {

    foreach($matches as $match) {
    list($block, $pre, $type, $param, $optional) = $match;

    if ($pre) {
    $block = substr($block, 1);
    }

    if(isset($params[$param])) {
    $url = str_replace($block, $params[$param], $url);
    } elseif ($optional) {
    $url = str_replace($pre . $block, '', $url);
    }
    }


    }

    return $url;
    }

    /**
    * Match a given Request Url against stored routes
    * @param string $requestUrl
    * @param string $requestMethod
    * @return array|boolean Array with route information on success, false on failure (no match).
    */
    public function match($requestUrl = null, $requestMethod = null) {

    $params = array();
    $match = false;

    // set Request Url if it isn't passed as parameter
    if($requestUrl === null) {
    $requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
    }

    // strip base path from request url
    $requestUrl = substr($requestUrl, strlen($this->basePath));

    // Strip query string (?a=b) from Request Url
    if (($strpos = strpos($requestUrl, '?')) !== false) {
    $requestUrl = substr($requestUrl, 0, $strpos);
    }

    // set Request Method if it isn't passed as a parameter
    if($requestMethod === null) {
    $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
    }

    // Force request_order to be GP
    // http://www.mail-archive.com/internals@lists.php.net/msg33119.html
    $_REQUEST = array_merge($_GET, $_POST);

    foreach($this->routes as $handler) {
    list($method, $_route, $target, $name) = $handler;

    $methods = explode('|', $method);
    $method_match = false;

    // Check if request method matches. If not, abandon early. (CHEAP)
    foreach($methods as $method) {
    if (strcasecmp($requestMethod, $method) === 0) {
    $method_match = true;
    break;
    }
    }

    // Method did not match, continue to next route.
    if(!$method_match) continue;

    // Check for a wildcard (matches all)
    if ($_route === '*') {
    $match = true;
    } elseif (is_callable($_route)) {
    $match = $_route();
    } elseif (isset($_route[0]) && $_route[0] === '@') {
    $pattern = '`' . substr($_route, 1) . '`u';
    $match = preg_match($pattern, $requestUrl, $params);
    } else {
    $route = null;
    $regex = false;
    $j = 0;
    $n = isset($_route[0]) ? $_route[0] : null;
    $i = 0;

    // Find the longest non-regex substring and match it against the URI
    while (true) {
    if (!isset($_route[$i])) {
    break;
    } elseif (false === $regex) {
    $c = $n;
    $regex = $c === '[' || $c === '(' || $c === '.';
    if (false === $regex && false !== isset($_route[$i+1])) {
    $n = $_route[$i + 1];
    $regex = $n === '?' || $n === '+' || $n === '*' || $n === '{';
    }
    if (false === $regex && $c !== '/' && (!isset($requestUrl[$j]) || $c !== $requestUrl[$j])) {
    continue 2;
    }
    $j++;
    }
    $route .= $_route[$i++];
    }

    $regex = $this->compileRoute($route);
    $match = preg_match($regex, $requestUrl, $params);
    }

    if(($match == true || $match > 0)) {

    if($params) {
    foreach($params as $key => $value) {
    if(is_numeric($key)) unset($params[$key]);
    }
    }

    return array(
    'target' => $target,
    'params' => $params,
    'name' => $name
    );
    }
    }
    return false;
    }

    /**
    * Compile the regex for a given route (EXPENSIVE)
    */
    private function compileRoute($route) {
    if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) {

    $matchTypes = $this->matchTypes;
    foreach($matches as $match) {
    list($block, $pre, $type, $param, $optional) = $match;

    if (isset($matchTypes[$type])) {
    $type = $matchTypes[$type];
    }
    if ($pre === '.') {
    $pre = '\.';
    }

    //Older versions of PCRE require the 'P' in (?P<named>)
    $pattern = '(?:'
    . ($pre !== '' ? $pre : null)
    . '('
    . ($param !== '' ? "?P<$param>" : null)
    . $type
    . '))'
    . ($optional !== '' ? '?' : null);

    $route = str_replace($block, $pattern, $route);
    }

    }
    return "`^$route$`u";
    }
    }