1. sfPatternRouting.class.php
  2. /** * sfPatternRouting class controls the generation and parsing of URLs. * * It parses and generates URLs by delegating the work to an array of sfRoute objects. * * @package symfony * @subpackage routing * @author Fabien Potencier * @version SVN: $Id: sfPatternRouting.class.php 24061 2009-11-16 22:35:03Z FabianLange $ */
  3. class sfPatternRouting extends sfRouting
  4. {
  5. protected
  6. $currentRouteName = null,
  7. $currentInternalUri = array(),
  8. $routes = array(),
  9. $defaultParamsDirty = false,
  10. $cacheData = array(),
  11. $cacheChanged = false;
  12. /**
  13. * Initializes this Routing.
  14. *
  15. * Available options:
  16. *
  17. * * suffix: The default suffix
  18. * * variable_prefixes: An array of characters that starts a variable name (: by default)
  19. * * segment_separators: An array of allowed characters for segment separators (/ and . by default)
  20. * * variable_regex: A regex that match a valid variable name ([\w\d_]+ by default)
  21. * * generate_shortest_url: Whether to generate the shortest URL possible (true by default)
  22. * * extra_parameters_as_query_string: Whether to generate extra parameters as a query string
  23. * * lookup_cache_dedicated_keys: Whether to use dedicated keys for parse/generate cache (false by default)
  24. * WARNING: When this option is activated, do not use sfFileCache; use a fast access
  25. * cache backend (like sfAPCCache).
  26. *
  27. * @see sfRouting
  28. */
  29. public function initialize(sfEventDispatcher $dispatcher, sfCache $cache = null, $options = array())
  30. {
  31. $options = array_merge(array(
  32. 'variable_prefixes' => array(':'),
  33. 'segment_separators' => array('/', '.'),
  34. 'variable_regex' => '[\w\d_]+',
  35. 'load_configuration' => false,
  36. 'suffix' => '',
  37. 'generate_shortest_url' => true,
  38. 'extra_parameters_as_query_string' => true,
  39. 'lookup_cache_dedicated_keys' => false,
  40. ), $options);
  41. // for BC
  42. if ('.' == $options['suffix'])
  43. {
  44. $options['suffix'] = '';
  45. }
  46. parent::initialize($dispatcher, $cache, $options);
  47. if (null !== $this->cache && !$options['lookup_cache_dedicated_keys'] && $cacheData = $this->cache->get('symfony.routing.data'))
  48. {
  49. $this->cacheData = unserialize($cacheData);
  50. }
  51. }
  52. /**
  53. * @see sfRouting
  54. */
  55. public function loadConfiguration()
  56. {
  57. if ($this->options['load_configuration'] && $config = $this->getConfigFilename())
  58. {
  59. include($config);
  60. }
  61. parent::loadConfiguration();
  62. }
  63. /**
  64. * Added for better performance. We need to ensure that changed default parameters
  65. * are set, but resetting them everytime wastes many cpu cycles
  66. */
  67. protected function ensureDefaultParametersAreSet()
  68. {
  69. if ($this->defaultParamsDirty)
  70. {
  71. foreach ($this->routes as $route)
  72. {
  73. $route->setDefaultParameters($this->defaultParameters);
  74. }
  75. $this->defaultParamsDirty = false;
  76. }
  77. }
  78. /**
  79. * @see sfRouting
  80. */
  81. public function setDefaultParameter($key, $value)
  82. {
  83. parent::setDefaultParameter($key, $value);
  84. $this->defaultParamsDirty = true;
  85. }
  86. /**
  87. * @see sfRouting
  88. */
  89. public function setDefaultParameters($parameters)
  90. {
  91. parent::setDefaultParameters($parameters);
  92. $this->defaultParamsDirty = true;
  93. }
  94. protected function getConfigFileName()
  95. {
  96. return sfContext::getInstance()->getConfigCache()->checkConfig('config/routing.yml', true);
  97. }
  98. /**
  99. * @see sfRouting
  100. */
  101. public function getCurrentInternalUri($withRouteName = false)
  102. {
  103. return null === $this->currentRouteName ? null : $this->currentInternalUri[$withRouteName ? 0 : 1];
  104. }
  105. /**
  106. * Gets the current route name.
  107. *
  108. * @return string The route name
  109. */
  110. public function getCurrentRouteName()
  111. {
  112. return $this->currentRouteName;
  113. }
  114. /**
  115. * @see sfRouting
  116. */
  117. public function getRoutes()
  118. {
  119. return $this->routes;
  120. }
  121. /**
  122. * @see sfRouting
  123. */
  124. public function setRoutes($routes)
  125. {
  126. foreach ($routes as $name => $route)
  127. {
  128. $this->connect($name, $route);
  129. }
  130. }
  131. /**
  132. * @see sfRouting
  133. */
  134. public function hasRoutes()
  135. {
  136. return count($this->routes) ? true : false;
  137. }
  138. /**
  139. * @see sfRouting
  140. */
  141. public function clearRoutes()
  142. {
  143. if ($this->options['logging'])
  144. {
  145. $this->dispatcher->notify(new sfEvent($this, 'application.log', array('Clear all current routes')));
  146. }
  147. $this->routes = array();
  148. }
  149. /**
  150. * Returns true if the route name given is defined.
  151. *
  152. * @param string $name The route name
  153. *
  154. * @return boolean
  155. */
  156. public function hasRouteName($name)
  157. {
  158. return isset($this->routes[$name]) ? true : false;
  159. }
  160. /**
  161. * Adds a new route at the beginning of the current list of routes.
  162. *
  163. * @see connect
  164. */
  165. public function prependRoute($name, $route)
  166. {
  167. $routes = $this->routes;
  168. $this->routes = array();
  169. $this->connect($name, $route);
  170. $this->routes = array_merge($this->routes, $routes);
  171. }
  172. /**
  173. * Adds a new route.
  174. *
  175. * Alias for the connect method.
  176. *
  177. * @see connect
  178. */
  179. public function appendRoute($name, $route)
  180. {
  181. return $this->connect($name, $route);
  182. }
  183. /**
  184. * Adds a new route before a given one in the current list of routes.
  185. *
  186. * @see connect
  187. */
  188. public function insertRouteBefore($pivot, $name, $route)
  189. {
  190. if (!isset($this->routes[$pivot]))
  191. {
  192. throw new sfConfigurationException(sprintf('Unable to insert route "%s" before inexistent route "%s".', $name, $pivot));
  193. }
  194. $routes = $this->routes;
  195. $this->routes = array();
  196. $newroutes = array();
  197. foreach ($routes as $key => $value)
  198. {
  199. if ($key == $pivot)
  200. {
  201. $this->connect($name, $route);
  202. $newroutes = array_merge($newroutes, $this->routes);
  203. }
  204. $newroutes[$key] = $value;
  205. }
  206. $this->routes = $newroutes;
  207. }
  208. /**
  209. * Adds a new route at the end of the current list of routes.
  210. *
  211. * A route string is a string with 2 special constructions:
  212. * - :string: :string denotes a named paramater (available later as $request->getParameter('string'))
  213. * - *: * match an indefinite number of parameters in a route
  214. *
  215. * Here is a very common rule in a symfony project:
  216. *
  217. * <code>
  218. * $r->connect('default', new sfRoute('/:module/:action/*'));
  219. * </code>
  220. *
  221. * @param string $name The route name
  222. * @param sfRoute $route A sfRoute instance
  223. *
  224. * @return array current routes
  225. */
  226. public function connect($name, $route)
  227. {
  228. $routes = $route instanceof sfRouteCollection ? $route : array($name => $route);
  229. foreach (self::flattenRoutes($routes) as $name => $route)
  230. {
  231. $this->routes[$name] = $route;
  232. $this->configureRoute($route);
  233. if ($this->options['logging'])
  234. {
  235. $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Connect %s "%s" (%s)', get_class($route), $name, $route->getPattern()))));
  236. }
  237. }
  238. }
  239. public function configureRoute(sfRoute $route)
  240. {
  241. $route->setDefaultParameters($this->defaultParameters);
  242. $route->setDefaultOptions($this->options);
  243. }
  244. /**
  245. * @see sfRouting
  246. */
  247. public function generate($name, $params = array(), $absolute = false)
  248. {
  249. // fetch from cache
  250. if (null !== $this->cache)
  251. {
  252. $cacheKey = 'generate_'.$name.'_'.md5(serialize(array_merge($this->defaultParameters, $params))).'_'.md5(serialize($this->options['context']));
  253. if ($this->options['lookup_cache_dedicated_keys'] && $url = $this->cache->get('symfony.routing.data.'.$cacheKey))
  254. {
  255. return $this->fixGeneratedUrl($url, $absolute);
  256. }
  257. elseif (isset($this->cacheData[$cacheKey]))
  258. {
  259. return $this->fixGeneratedUrl($this->cacheData[$cacheKey], $absolute);
  260. }
  261. }
  262. if ($name)
  263. {
  264. // named route
  265. if (!isset($this->routes[$name]))
  266. {
  267. throw new sfConfigurationException(sprintf('The route "%s" does not exist.', $name));
  268. }
  269. $route = $this->routes[$name]; $this->ensureDefaultParametersAreSet();
  270. }
  271. else
  272. {
  273. // find a matching route
  274. if (false === $route = $this->getRouteThatMatchesParameters($params, $this->options['context']))
  275. {
  276. throw new sfConfigurationException(sprintf('Unable to find a matching route to generate url for params "%s".', is_object($params) ? 'Object('.get_class($params).')' : str_replace("\n", '', var_export($params, true))));
  277. }
  278. }
  279. $url = $route->generate($params, $this->options['context'], $absolute);
  280. // store in cache
  281. if (null !== $this->cache)
  282. {
  283. if ($this->options['lookup_cache_dedicated_keys'])
  284. {
  285. $this->cache->set('symfony.routing.data.'.$cacheKey, $url);
  286. }
  287. else
  288. {
  289. $this->cacheChanged = true;
  290. $this->cacheData[$cacheKey] = $url;
  291. }
  292. }
  293. return $this->fixGeneratedUrl($url, $absolute);
  294. }
  295. /**
  296. * @see sfRouting
  297. */
  298. public function parse($url)
  299. {
  300. if (false === $info = $this->findRoute($url))
  301. {
  302. $this->currentRouteName = null;
  303. $this->currentInternalUri = array();
  304. return false;
  305. }
  306. if ($this->options['logging'])
  307. {
  308. $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Match route "%s" (%s) for %s with parameters %s', $info['name'], $info['pattern'], $url, str_replace("\n", '', var_export($info['parameters'], true))))));
  309. }
  310. // store the current internal URI
  311. $this->updateCurrentInternalUri($info['name'], $info['parameters']);
  312. $route = $this->routes[$info['name']];
  313. $this->ensureDefaultParametersAreSet();
  314. $route->bind($this->options['context'], $info['parameters']);
  315. $info['parameters']['_sf_route'] = $route;
  316. return $info['parameters'];
  317. }
  318. protected function updateCurrentInternalUri($name, array $parameters)
  319. {
  320. // store the route name
  321. $this->currentRouteName = $name;
  322. $internalUri = array('@'.$this->currentRouteName, $parameters['module'].'/'.$parameters['action']);
  323. unset($parameters['module'], $parameters['action']);
  324. $params = array();
  325. foreach ($parameters as $key => $value)
  326. {
  327. $params[] = $key.'='.$value;
  328. }
  329. // sort to guaranty unicity
  330. sort($params);
  331. $params = $params ? '?'.implode('&', $params) : '';
  332. $this->currentInternalUri = array($internalUri[0].$params, $internalUri[1].$params);
  333. }
  334. /**
  335. * Finds a matching route for given URL.
  336. *
  337. * Returns false if no route matches.
  338. *
  339. * Returned array contains:
  340. *
  341. * - name: name or alias of the route that matched
  342. * - pattern: the compiled pattern of the route that matched
  343. * - parameters: array containing key value pairs of the request parameters including defaults
  344. *
  345. * @param string $url URL to be parsed
  346. *
  347. * @return array|false An array with routing information or false if no route matched
  348. */
  349. public function findRoute($url)
  350. {
  351. $url = $this->normalizeUrl($url);
  352. // fetch from cache
  353. if (null !== $this->cache)
  354. {
  355. $cacheKey = 'parse_'.$url.'_'.md5(serialize($this->options['context']));
  356. if ($this->options['lookup_cache_dedicated_keys'] && $info = $this->cache->get('symfony.routing.data.'.$cacheKey))
  357. {
  358. return unserialize($info);
  359. }
  360. elseif (isset($this->cacheData[$cacheKey]))
  361. {
  362. return $this->cacheData[$cacheKey];
  363. }
  364. }
  365. $info = $this->getRouteThatMatchesUrl($url);
  366. // store in cache
  367. if (null !== $this->cache)
  368. {
  369. if ($this->options['lookup_cache_dedicated_keys'])
  370. {
  371. $this->cache->set('symfony.routing.data.'.$cacheKey, serialize($info));
  372. }
  373. else
  374. {
  375. $this->cacheChanged = true;
  376. $this->cacheData[$cacheKey] = $info;
  377. }
  378. }
  379. return $info;
  380. }
  381. static public function flattenRoutes($routes)
  382. {
  383. $flattenRoutes = array();
  384. foreach ($routes as $name => $route)
  385. {
  386. if ($route instanceof sfRouteCollection)
  387. {
  388. $flattenRoutes = array_merge($flattenRoutes, self::flattenRoutes($route));
  389. }
  390. else
  391. {
  392. $flattenRoutes[$name] = $route;
  393. }
  394. }
  395. return $flattenRoutes;
  396. }
  397. protected function getRouteThatMatchesUrl($url)
  398. {
  399. $this->ensureDefaultParametersAreSet();
  400. foreach ($this->routes as $name => $route)
  401. {
  402. if (false === $parameters = $route->matchesUrl($url, $this->options['context']))
  403. {
  404. continue;
  405. }
  406. return array('name' => $name, 'pattern' => $route->getPattern(), 'parameters' => $parameters);
  407. }
  408. return false;
  409. }
  410. protected function getRouteThatMatchesParameters($parameters)
  411. {
  412. $this->ensureDefaultParametersAreSet();
  413. foreach ($this->routes as $route)
  414. {
  415. if ($route->matchesParameters($parameters, $this->options['context']))
  416. {
  417. return $route;
  418. }
  419. }
  420. return false;
  421. }
  422. protected function normalizeUrl($url)
  423. {
  424. // an URL should start with a '/', mod_rewrite doesn't respect that, but no-mod_rewrite version does.
  425. if ('/' != substr($url, 0, 1))
  426. {
  427. $url = '/'.$url;
  428. }
  429. // we remove the query string
  430. if (false !== $pos = strpos($url, '?'))
  431. {
  432. $url = substr($url, 0, $pos);
  433. }
  434. // remove multiple /
  435. $url = preg_replace('#/+#', '/', $url);
  436. return $url;
  437. }
  438. /**
  439. * @see sfRouting
  440. */
  441. public function shutdown()
  442. {
  443. if (null !== $this->cache && $this->cacheChanged)
  444. {
  445. $this->cacheChanged = false;
  446. $this->cache->set('symfony.routing.data', serialize($this->cacheData));
  447. }
  448. }
  449. }

Debug toolbar