[ create a new paste ] login | about

Link: http://codepad.org/lrMiixX7    [ raw code | fork ]

k4st - PHP, pasted on May 25:
<?php

error_reporting(E_ALL | E_STRICT);

!defined("APPPATH") && define("APPPATH", dirname(__FILE__));
!defined("EXT") && define("EXT", '.php');

/**
 * Deal with parsing routes. This class accepts programmer defined route
 * remappings. With these remappings, the router groups them by their longest
 * prefix of terminals (things that won't change such as /'s and words) and
 * then matches against those. When the router can't match against a route
 * in memory, it will simply scan the document tree until it can find a
 * controller.
 * @author Peter Goodman
 */
class Router implements ArrayAccess {
    
    // storage and other things
    protected $macro_keys = array(), // instead of constantly doing
              $macro_vals = array(), // array_keys and array_values
              $routes = array(), // stored routes, see addRoute
              $path = array();   // information about the current path
    
    // so that it will fit nicely into CodeIgniter / Kohana
    public $allowed_chars = "a-zA-Z0-9_-",
           $arguments = array(), // arguments passed through the route
           $directory = '',      // the directory where the controller is
           $controller, // the controller class to instantiate
           $method;     // the method of the controller to call
    
    /**
     * Constructor, build up some default macros.
     */
    public function __construct() {
        $this->addMacro('alpha', '[a-zA-Z]+');
        $this->addMacro('num', '[0-9]+');
        $this->addMacro('alphanum', '[a-zA-Z0-9]+');
        $this->addMacro('any', '.*');
        $this->addMacro('word', '\w+');
        $this->addMacro('year', '[12][0-9]{3}');
        $this->addMacro('month', '0[1-9]|1[012]');
        $this->addMacro('day', '0[1-9]|[12][0-9]|3[01]');
        $this->addMacro('id', '[0-9]+');
        $this->addMacro('uuid', '[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]'.
                                '{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}');
    }
    
    /**
     * Add a macro that should be expanded into a regular expression.
     */
    public function addMacro($id, $regex) {
        $this->macro_keys[] = '(:'. $id .')';
        $this->macro_vals[] = '('. $regex .')'; // force it to be a subpattern
    }
    
    /**
     * Clean up the route a bit: take away useless spaces and /'s.
     */
    protected function cleanPath($route) {
        return '/'. trim(preg_replace('~\s*[/]+\s*~', '/', $route), '/');
    }
    
    /**
     * Get the longest prefix of terminals for a given route added through
     * addRoute.
     */
    protected function calculateLongestPrefix($route) {
        $matches = array();
        $bad = '/'; // worst-case prefix
        
        // get the longest prefix of terminals in this route
        if(preg_match("~(([". $this->allowed_chars ."]|/)+)~", $route, $matches))
            return !empty($matches[0]) ? $matches[0] : $bad;
                
        return $bad;
    }
    
    /**
     * Given all of the prefixes, find the longest matching one for a given
     * route.
     */
    protected function getLongestMatchingPrefix($route) {
        
        // we break the route up into segments, and we also set those segments
        // and the $segments array      
        if(0 == count($parts = explode('/', $route)))
            return '/';
        
        $prefix = $temp = '';
        $i = 0;
        
        // build up the longest prefix incrementally
        do {
            $prefix = $temp;
            $temp .= '/'. $parts[$i++];
        } while(isset($parts[$i]) && isset($this->routes[$temp]));
        
        return '/';
    }
    
    /**
     * Add a route for parsing. Remapping has two uses. First, we might be
     * mapping a route to itself so that we can properly sanitize the
     * incoming data, or we might be remapping a route, that is: creating a
     * special way to access a controller's action without necessarily
     * having a one-to-one relationship between the route and the path to the
     * controller+action.
     */
    public function addRoute($route, $maps_to) {
            
        // clean up the route and what it maps to
        $route = $this->cleanPath($route);
        $maps_to = $this->cleanPath($maps_to);
        
        // get the prefix of the route that is the sum of terminals
        $prefix = $this->calculateLongestPrefix($route);
        
        // replace macros. Even though the normal macro can't be matched as
        // a terminal, it's possible that the macro is in fact a terminal,
        // even though this scenario seems redundant, we will replace macros
        // right now.
        $route = str_replace($this->macro_keys, $this->macro_vals, $route);
        
        // make sure we group all routes with the same prefix together, that
        // way when we encounter a route, we only search given its prefix
        if(!isset($this->routes[$prefix]))
            $this->routes[$prefix] = array();
        
        // add in the route to others with similar prefixes. we can also
        // disgard the prefix from the route as it is useless to us.
        $route = substr($route, strlen($prefix));
        $this->routes[$prefix][] = array($route, $maps_to);     
    }
    
    /**
     * Parse a route into several segments. We make one central assumption:
     * that the only routes with ordered arguments are ones that have been
     * added in memory. Otherwise, we will just take anything after the 
     * method (action) of the controller and consider it an argument.
     */
    public function parseRoute($route) {
        
        // clean up the incoming route and calculate its prefix
        $route = $path = trim($this->cleanPath($route), '/');
        $prefix = $this->getLongestMatchingPrefix($route);
        
        // this will hold intermediate arguments
        $dynamic = array();
        
        // we're dealing with a route remapping so we want to find the route
        // we should actually be parsing. At the same time, we need to realize
        // that there might be dynamic elements in this incoming route that
        // need to be passed into the proper route mapping. We'll assume that
        // such dynamic elements are ordered correctly.
        if(isset($this->routes[$prefix])) {
            
            // we don't want to mangle the actual route because we might not
            // find what we're looking for in here, so we'll work with a
            // temporary copy of the route
            $temp = substr($route, strlen($prefix)-1);
            $matches = array();
            
            // lets see if we have this route in memory, if not we will fall
            // through this if and assume that the route points directly to
            // a controller and an action.
            foreach($this->routes[$prefix] as $suffix) {
                
                // remember, we store the route and what it maps to
                list($pattern, $maps_to) = $suffix;
                
                // does this route match any patterns?
                if(1 > preg_match('~'.$pattern.'~', $temp, $matches))
                    continue;

                // make sure we get the arguments in the right order
                $this->reorderArguments($maps_to, $matches);
                $path = trim($maps_to, '/');
                break;
            }
        }
        
        // now that we (might) have collected the arguments, lets try to find
        // what controller it should belong to by searching through $path. The
        // extra /'s act as centinels to make sure that controller and method
        // will be found.
        $path_parts = explode('/', ltrim($path, '/') .'///');
        
        $i = -1;
        $base = APPPATH .'/controllers/';
        
        // build up the directory to the controller, making sure to ignore
        // empty sub-directories
        while(!empty($path_parts[++$i]) && 
              is_dir($base . $this->directory . $path_parts[$i]))
            $this->directory .= '/'. $path_parts[++$i];
        
        // now populate the rest of the rsegments array
        $this->controller = $path_parts[$i++];
        $this->method = $path_parts[$i];

        // does the controller file exist?
        if(!file_exists($this->directory .'/'. $this->controller . EXT))
            return FALSE;
        
        return TRUE;
    }
    
    /**
     * It's possible that in the mapped route the arguments are in a different
     * order than they are in the actual route, and thus need to be sent to
     * the controller in a corrected order.
     */
    protected function reorderArguments($route, array &$sub) {
        
        // get rid of extraneous dollar signs that don't represent substituted
        // arguments (eg: $$1 -> $1)
        $route = preg_replace('~(\$(?![0-9]))~x', '', $route);
        
        // make sure we end up with the right number of arguments
        $this->arguments = array_fill(0, substr_count($route, '$'), NULL);
        
        // find all the variables within the route (in order)
        $matches = array();
        if(!preg_match_all('~\$([0-9]+)~', $route, $matches))
            return;
                
        // the controller expects a certain number of arguments, we might not
        // actually get that many though, but that's no longer an issue
        $count = count($this->arguments);
        $i = -1;
        
        // iterate over the found variables
        while(isset($matches[1][++$i])) {
            
            // we will use this index to look into the $sub array for the
            // value of the ith argument.
            $index = (int)$matches[1][$i];
                        
            // the route references an incorrect argument number, ignore it
            if(!isset($sub[$index]))
                continue;
            
            // put the argument in order.
            $this->arguments[$i] = $sub[$index];
        }
    }
    
    /**
     * Convenient way to add a route.
     */
    final public function offsetSet($route, $maps_to) {
        $this->addRoute($route, $maps_to);
    }
    final public function offsetGet($route) { return NULL; }
    final public function offsetExists($route) { return FALSE; }
    final public function offsetUnset($route) { }
}

$router = new Router;
$router['/controller/moo'] = '/controller/method/bar';
$router['/(:num)/(:alpha)'] = '/controller/method/$2/$$1';

if($router->parseRoute('/100/abcdefghi'))
    echo 'found controller';
else
    echo 'unable to locate controller, suggest 404 error.';

echo "\n";

print_r($router);


Create a new paste based on this one


Comments: