123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740 |
- <?php
- /*
- * This file is part of the symfony package.
- * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- /**
- * sfPatternRouting class controls the generation and parsing of URLs.
- *
- * It maps an array of parameters to URLs definition. Each map is called a route.
- *
- * @package symfony
- * @subpackage routing
- * @author Fabien Potencier <fabien.potencier@symfony-project.com>
- * @version SVN: $Id: sfPatternRouting.class.php 17746 2009-04-29 11:41:08Z fabien $
- */
- class sfPatternRouting extends sfRouting
- {
- protected
- $currentRouteName = null,
- $currentInternalUri = array(),
- $currentRouteParameters = null,
- $defaultSuffix = '',
- $routes = array(),
- $cacheData = array(),
- $cacheChanged = false;
- /**
- * Initializes this Routing.
- *
- * Available options:
- *
- * * suffix: The default suffix
- * * variable_prefixes: An array of characters that starts a variable name (: by default)
- * * segment_separators: An array of allowed characters for segment separators (/ and . by default)
- * * variable_regex: A regex that match a valid variable name ([\w\d_]+ by default)
- *
- * @see sfRouting
- */
- public function initialize(sfEventDispatcher $dispatcher, sfCache $cache = null, $options = array())
- {
- if (!isset($options['variable_prefixes']))
- {
- $options['variable_prefixes'] = array(':');
- }
- if (!isset($options['segment_separators']))
- {
- $options['segment_separators'] = array('/', '.');
- }
- if (!isset($options['variable_regex']))
- {
- $options['variable_regex'] = '[\w\d_]+';
- }
- $options['variable_prefix_regex'] = '(?:'.implode('|', array_map(create_function('$a', 'return preg_quote($a, \'#\');'), $options['variable_prefixes'])).')';
- $options['segment_separators_regex'] = '(?:'.implode('|', array_map(create_function('$a', 'return preg_quote($a, \'#\');'), $options['segment_separators'])).')';
- $options['variable_content_regex'] = '[^'.implode('', array_map(create_function('$a', 'return str_replace(\'-\', \'\-\', preg_quote($a, \'#\'));'), $options['segment_separators'])).']+';
- if (!isset($options['load_configuration']))
- {
- $options['load_configuration'] = false;
- }
- $this->setDefaultSuffix(isset($options['suffix']) ? $options['suffix'] : '');
- parent::initialize($dispatcher, $cache, $options);
- if (!is_null($this->cache) && $cacheData = $this->cache->get('symfony.routing.data'))
- {
- $this->cacheData = unserialize($cacheData);
- }
- }
- /**
- * @see sfRouting
- */
- public function loadConfiguration()
- {
- if (!is_null($this->cache) && $routes = $this->cache->get('symfony.routing.configuration'))
- {
- $this->routes = unserialize($routes);
- }
- else
- {
- if ($this->options['load_configuration'] && $config = sfContext::getInstance()->getConfigCache()->checkConfig('config/routing.yml', true))
- {
- include($config);
- }
- parent::loadConfiguration();
- if (!is_null($this->cache))
- {
- $this->cache->set('symfony.routing.configuration', serialize($this->routes));
- }
- }
- }
- /**
- * @see sfRouting
- */
- public function getCurrentInternalUri($withRouteName = false)
- {
- if (is_null($this->currentRouteName))
- {
- return null;
- }
- $typeId = $withRouteName ? 0 : 1;
- if (!isset($this->currentInternalUri[$typeId]))
- {
- $parameters = $this->currentRouteParameters;
- list($url, $regex, $variables, $defaults, $requirements) = $this->routes[$this->currentRouteName];
- $internalUri = $withRouteName ? '@'.$this->currentRouteName : $parameters['module'].'/'.$parameters['action'];
- $params = array();
- // add parameters
- foreach (array_keys(array_merge($defaults, $variables)) as $variable)
- {
- if ($variable == 'module' || $variable == 'action')
- {
- continue;
- }
- $params[] = $variable.'='.(isset($parameters[$variable]) ? $parameters[$variable] : (isset($defaults[$variable]) ? $defaults[$variable] : ''));
- }
- // add * parameters if needed
- if (false !== strpos($regex, '_star'))
- {
- foreach ($parameters as $key => $value)
- {
- if ($key == 'module' || $key == 'action' || isset($variables[$key]))
- {
- continue;
- }
- $params[] = $key.'='.$value;
- }
- }
- // sort to guaranty unicity
- sort($params);
- $this->currentInternalUri[$typeId] = $internalUri.($params ? '?'.implode('&', $params) : '');
- }
- return $this->currentInternalUri[$typeId];
- }
- /**
- * Gets the current route name.
- *
- * @return string The route name
- */
- public function getCurrentRouteName()
- {
- return $this->currentRouteName;
- }
- /**
- * Sets the default suffix
- *
- * @param string The default suffix
- */
- public function setDefaultSuffix($suffix)
- {
- $this->defaultSuffix = '.' == $suffix ? '' : $suffix;
- }
- /**
- * @see sfRouting
- */
- public function getRoutes()
- {
- return $this->routes;
- }
- /**
- * @see sfRouting
- */
- public function setRoutes($routes)
- {
- return $this->routes = $routes;
- }
- /**
- * @see sfRouting
- */
- public function hasRoutes()
- {
- return count($this->routes) ? true : false;
- }
- /**
- * @see sfRouting
- */
- public function clearRoutes()
- {
- if ($this->options['logging'])
- {
- $this->dispatcher->notify(new sfEvent($this, 'application.log', array('Clear all current routes')));
- }
- $this->routes = array();
- }
- /**
- * Returns true if the route name given is defined.
- *
- * @param string $name The route name
- *
- * @return boolean
- */
- public function hasRouteName($name)
- {
- return isset($this->routes[$name]) ? true : false;
- }
- /**
- * Adds a new route at the beginning of the current list of routes.
- *
- * @see connect
- */
- public function prependRoute($name, $route, $default = array(), $requirements = array())
- {
- $routes = $this->routes;
- $this->routes = array();
- $newroutes = $this->connect($name, $route, $default, $requirements);
- $this->routes = array_merge($newroutes, $routes);
- return $this->routes;
- }
- /**
- * Adds a new route.
- *
- * Alias for the connect method.
- *
- * @see connect
- */
- public function appendRoute($name, $route, $default = array(), $requirements = array())
- {
- return $this->connect($name, $route, $default, $requirements);
- }
- /**
- * Adds a new route before a given one in the current list of routes.
- *
- * @see connect
- */
- public function insertRouteBefore($pivot, $name, $route, $default = array(), $requirements = array())
- {
- if (!isset($this->routes[$pivot]))
- {
- throw new sfConfigurationException(sprintf('Unable to insert route "%s" before inexistent route "%s".', $name, $pivot));
- }
- $routes = $this->routes;
- $this->routes = array();
- $newroutes = array();
- foreach ($routes as $key => $value)
- {
- if ($key == $pivot)
- {
- $newroutes = array_merge($newroutes, $this->connect($name, $route, $default, $requirements));
- }
- $newroutes[$key] = $value;
- }
- return $this->routes = $newroutes;
- }
- /**
- * Adds a new route at the end of the current list of routes.
- *
- * A route string is a string with 2 special constructions:
- * - :string: :string denotes a named paramater (available later as $request->getParameter('string'))
- * - *: * match an indefinite number of parameters in a route
- *
- * Here is a very common rule in a symfony project:
- *
- * <code>
- * $r->connect('default', '/:module/:action/*');
- * </code>
- *
- * @param string $name The route name
- * @param string $route The route string
- * @param array $defaults The default parameter values
- * @param array $requirements The regexps parameters must match
- *
- * @return array current routes
- */
- public function connect($name, $route, $defaults = array(), $requirements = array())
- {
- // route already exists?
- if (isset($this->routes[$name]))
- {
- throw new sfConfigurationException(sprintf('This named route already exists ("%s").', $name));
- }
- $suffix = $this->defaultSuffix;
- $route = trim($route);
- // fix defaults
- foreach ($defaults as $key => $value)
- {
- if (ctype_digit($key))
- {
- $defaults[$value] = true;
- }
- else
- {
- $defaults[$key] = urldecode($value);
- }
- }
- $givenDefaults = $defaults;
- $defaults = $this->fixDefaults($defaults);
- // fix requirements regexs
- foreach ($requirements as $key => $regex)
- {
- if ('^' == $regex[0])
- {
- $regex = substr($regex, 1);
- }
- if ('$' == substr($regex, -1))
- {
- $regex = substr($regex, 0, -1);
- }
- $requirements[$key] = $regex;
- }
- // a route can start by a slash. remove it for parsing.
- if (!empty($route) && '/' == $route[0])
- {
- $route = substr($route, 1);
- }
- if ($route == '')
- {
- $this->routes[$name] = array('/', '/^\/*$/', array(), $defaults, $requirements);
- }
- else
- {
- // ignore the default suffix if one is already provided in the route
- if ('/' == $route[strlen($route) - 1])
- {
- // route ends by / (directory)
- $suffix = '';
- }
- else if ('.' == $route[strlen($route) - 1])
- {
- // route ends by . (no suffix)
- $suffix = '';
- $route = substr($route, 0, strlen($route) -1);
- }
- else if (preg_match('#\.(?:'.$this->options['variable_prefix_regex'].$this->options['variable_regex'].'|'.$this->options['variable_content_regex'].')$#i', $route))
- {
- // specific suffix for this route
- // a . with a variable after or some cars without any separators
- $suffix = '';
- }
- // parse the route
- $segments = array();
- $firstOptional = 0;
- $buffer = $route;
- $afterASeparator = true;
- $currentSeparator = '';
- $variables = array();
- // a route is an array of (separator + variable) or (separator + text) segments
- while (strlen($buffer))
- {
- if ($afterASeparator && preg_match('#^'.$this->options['variable_prefix_regex'].'('.$this->options['variable_regex'].')#', $buffer, $match))
- {
- // a variable (like :foo)
- $variable = $match[1];
- if (!isset($requirements[$variable]))
- {
- $requirements[$variable] = $this->options['variable_content_regex'];
- }
- $segments[] = $currentSeparator.'(?P<'.$variable.'>'.$requirements[$variable].')';
- $currentSeparator = '';
- // for 1.0 BC, we don't take into account the default module and action variable
- // for 1.2, remove the $givenDefaults var and move the $firstOptional setting to
- // the condition below
- if (!isset($givenDefaults[$variable]))
- {
- $firstOptional = count($segments);
- }
- if (!isset($defaults[$variable]))
- {
- $defaults[$variable] = null;
- }
- $buffer = substr($buffer, strlen($match[0]));
- $variables[$variable] = $match[0];
- $afterASeparator = false;
- }
- else if ($afterASeparator)
- {
- // a static text
- if (!preg_match('#^(.+?)(?:'.$this->options['segment_separators_regex'].'|$)#', $buffer, $match))
- {
- throw new InvalidArgumentException(sprintf('Unable to parse "%s" route near "%s".', $route, $buffer));
- }
- if ('*' == $match[1])
- {
- $segments[] = '(?:'.$currentSeparator.'(?P<_star>.*))?';
- }
- else
- {
- $segments[] = $currentSeparator.preg_quote($match[1], '#');
- $firstOptional = count($segments);
- }
- $currentSeparator = '';
- $buffer = substr($buffer, strlen($match[1]));
- $afterASeparator = false;
- }
- else if (preg_match('#^'.$this->options['segment_separators_regex'].'#', $buffer, $match))
- {
- // a separator (like / or .)
- $currentSeparator = preg_quote($match[0], '#');
- $buffer = substr($buffer, strlen($match[0]));
- $afterASeparator = true;
- }
- else
- {
- // parsing problem
- throw new InvalidArgumentException(sprintf('Unable to parse "%s" route near "%s".', $route, $buffer));
- }
- }
- // all segments after the last static segment are optional
- // be careful, the n-1 is optional only if n is empty
- for ($i = $firstOptional, $max = count($segments); $i < $max; $i++)
- {
- $segments[$i] = str_repeat(' ', $i - $firstOptional).'(?:'.$segments[$i];
- $segments[] = str_repeat(' ', $max - $i - 1).')?';
- }
- $regex = "#^/\n".implode("\n", $segments)."\n".$currentSeparator.preg_quote($suffix, '#')."$#x";
- $this->routes[$name] = array('/'.$route.$suffix, $regex, $variables, $defaults, $requirements);
- }
- if ($this->options['logging'])
- {
- $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Connect "/%s"%s', $route, $suffix ? ' ("'.$suffix.'" suffix)' : ''))));
- }
- return $this->routes;
- }
- /**
- * @see sfRouting
- */
- public function generate($name, $params = array(), $querydiv = '/', $divider = '/', $equals = '/')
- {
- $params = $this->fixDefaults($params);
-
- if (!is_null($this->cache))
- {
- $cacheKey = 'generate_'.$name.serialize(array_merge($this->defaultParameters, $params));
- if (isset($this->cacheData[$cacheKey]))
- {
- return $this->cacheData[$cacheKey];
- }
- }
- // named route?
- if ($name)
- {
- if (!isset($this->routes[$name]))
- {
- throw new sfConfigurationException(sprintf('The route "%s" does not exist.', $name));
- }
- list($url, $regex, $variables, $defaults, $requirements) = $this->routes[$name];
- $defaults = $this->mergeArrays($defaults, $this->defaultParameters);
- $tparams = $this->mergeArrays($defaults, $params);
- // all params must be given
- if ($diff = array_diff_key($variables, array_filter($tparams, create_function('$v', 'return !is_null($v);'))))
- {
- throw new InvalidArgumentException(sprintf('The "%s" route has some missing mandatory parameters (%s).', $name, implode(', ', $diff)));
- }
- }
- else
- {
- // find a matching route
- $found = false;
- foreach ($this->routes as $name => $route)
- {
- list($url, $regex, $variables, $defaults, $requirements) = $route;
- $defaults = $this->mergeArrays($defaults, $this->defaultParameters);
- $tparams = $this->mergeArrays($defaults, $params);
- // all $variables must be defined in the $tparams array
- if (array_diff_key($variables, array_filter($tparams)))
- {
- continue;
- }
- // check requirements
- foreach ($requirements as $reqParam => $reqRegexp)
- {
- if (!is_null($tparams[$reqParam]) && !preg_match('#'.$reqRegexp.'#', $tparams[$reqParam]))
- {
- continue 2;
- }
- }
- // all $params must be in $variables or $defaults if there is no * in route
- if (false === strpos($regex, '_star') && array_diff_key(array_filter($params), $variables, $defaults))
- {
- continue;
- }
- // check that $params does not override a default value that is not a variable
- foreach (array_filter($defaults) as $key => $value)
- {
- if (!isset($variables[$key]) && $tparams[$key] != $value)
- {
- continue 2;
- }
- }
- // found
- $found = true;
- break;
- }
- if (!$found)
- {
- throw new sfConfigurationException(sprintf('Unable to find a matching routing rule to generate url for params "%s".', var_export($params, true)));
- }
- }
- // replace variables
- $realUrl = $url;
- $tmp = $variables;
- uasort($tmp, create_function('$a, $b', 'return strlen($a) < strlen($b);'));
- foreach ($tmp as $variable => $value)
- {
- $realUrl = str_replace($value, urlencode($tparams[$variable]), $realUrl);
- }
- // add extra params if the route contains *
- if (false !== strpos($regex, '_star'))
- {
- $tmp = array();
- foreach (array_diff_key($tparams, $variables, $defaults) as $key => $value)
- {
- if (is_array($value))
- {
- foreach ($value as $v)
- {
- $tmp[] = $key.$equals.urlencode($v);
- }
- }
- else
- {
- $tmp[] = urlencode($key).$equals.urlencode($value);
- }
- }
- $tmp = implode($divider, $tmp);
- if ($tmp)
- {
- $tmp = $querydiv.$tmp;
- }
- $realUrl = preg_replace('#'.$this->options['segment_separators_regex'].'\*('.$this->options['segment_separators_regex'].'|$)#', "$tmp$1", $realUrl);
- }
- if (!is_null($this->cache))
- {
- $this->cacheChanged = true;
- $this->cacheData[$cacheKey] = $realUrl;
- }
- return $realUrl;
- }
- /**
- * @see sfRouting
- */
- public function parse($url)
- {
- if (null !== $routeInfo = $this->findRoute($url))
- {
- // store the route name
- $this->currentRouteName = $routeInfo['name'];
- $this->currentRouteParameters = $routeInfo['parameters'];
- $this->currentInternalUri = array();
- if ($this->options['logging'])
- {
- $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Match route [%s] for "%s"', $routeInfo['name'], $routeInfo['route']))));
- }
- }
- else
- {
- throw new sfError404Exception(sprintf('No matching route found for "%s"', $url));
- }
- return $this->currentRouteParameters;
- }
- /**
- * Finds a matching route for given URL.
- *
- * Returned array contains:
- * - name : name or alias of the route that matched
- * - route : the actual matching route
- * - parameters : array containing key value pairs of the request parameters including defaults
- *
- * @param string $url URL to be parsed
- *
- * @return array An array with routing information or null if no route matched
- */
- public function findRoute($url)
- {
- // an URL should start with a '/', mod_rewrite doesn't respect that, but no-mod_rewrite version does.
- if ('/' != substr($url, 0, 1))
- {
- $url = '/'.$url;
- }
- // we remove the query string
- if (false !== $pos = strpos($url, '?'))
- {
- $url = substr($url, 0, $pos);
- }
- // remove multiple /
- $url = preg_replace('#/+#', '/', $url);
- if (!is_null($this->cache))
- {
- $cacheKey = 'parse_'.$url;
- if (isset($this->cacheData[$cacheKey]))
- {
- return $this->cacheData[$cacheKey];
- }
- }
- $routeInfo = null;
- foreach ($this->routes as $routeName => $route)
- {
- list($route, $regex, $variables, $defaults, $requirements) = $route;
- if (!preg_match($regex, $url, $r))
- {
- continue;
- }
- $defaults = array_merge($defaults, $this->defaultParameters);
- $out = array();
- // *
- if (isset($r['_star']))
- {
- $out = $this->parseStarParameter($r['_star']);
- unset($r['_star']);
- }
- // defaults
- $out = $this->mergeArrays($out, $defaults);
- // variables
- foreach ($r as $key => $value)
- {
- if (!is_int($key))
- {
- $out[$key] = urldecode($value);
- }
- }
- $routeInfo['name'] = $routeName;
- $routeInfo['route'] = $route;
- $routeInfo['parameters'] = $this->fixDefaults($out);
- if (!is_null($this->cache))
- {
- $this->cacheChanged = true;
- $this->cacheData[$cacheKey] = $routeInfo;
- }
- break;
- }
- return $routeInfo;
- }
- protected function parseStarParameter($star)
- {
- $parameters = array();
- $tmp = explode('/', $star);
- for ($i = 0, $max = count($tmp); $i < $max; $i += 2)
- {
- //dont allow a param name to be empty - #4173
- if (!empty($tmp[$i]))
- {
- $parameters[$tmp[$i]] = isset($tmp[$i + 1]) ? urldecode($tmp[$i + 1]) : true;
- }
- }
- return $parameters;
- }
- /**
- * @see sfRouting
- */
- public function shutdown()
- {
- if (!is_null($this->cache) && $this->cacheChanged)
- {
- $this->cacheChanged = false;
- $this->cache->set('symfony.routing.data', serialize($this->cacheData));
- }
- }
- }
|