codepad
[
create a new paste
]
login
|
about
Language:
C
C++
D
Haskell
Lua
OCaml
PHP
Perl
Plain Text
Python
Ruby
Scheme
Tcl
<?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);
Private
[
?
]
Run code