sfPatternRouting.class.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. <?php
  2. /*
  3. * This file is part of the symfony package.
  4. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  5. *
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. /**
  10. * sfPatternRouting class controls the generation and parsing of URLs.
  11. *
  12. * It maps an array of parameters to URLs definition. Each map is called a route.
  13. *
  14. * @package symfony
  15. * @subpackage routing
  16. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  17. * @version SVN: $Id: sfPatternRouting.class.php 17746 2009-04-29 11:41:08Z fabien $
  18. */
  19. class sfPatternRouting extends sfRouting
  20. {
  21. protected
  22. $currentRouteName = null,
  23. $currentInternalUri = array(),
  24. $currentRouteParameters = null,
  25. $defaultSuffix = '',
  26. $routes = array(),
  27. $cacheData = array(),
  28. $cacheChanged = false;
  29. /**
  30. * Initializes this Routing.
  31. *
  32. * Available options:
  33. *
  34. * * suffix: The default suffix
  35. * * variable_prefixes: An array of characters that starts a variable name (: by default)
  36. * * segment_separators: An array of allowed characters for segment separators (/ and . by default)
  37. * * variable_regex: A regex that match a valid variable name ([\w\d_]+ by default)
  38. *
  39. * @see sfRouting
  40. */
  41. public function initialize(sfEventDispatcher $dispatcher, sfCache $cache = null, $options = array())
  42. {
  43. if (!isset($options['variable_prefixes']))
  44. {
  45. $options['variable_prefixes'] = array(':');
  46. }
  47. if (!isset($options['segment_separators']))
  48. {
  49. $options['segment_separators'] = array('/', '.');
  50. }
  51. if (!isset($options['variable_regex']))
  52. {
  53. $options['variable_regex'] = '[\w\d_]+';
  54. }
  55. $options['variable_prefix_regex'] = '(?:'.implode('|', array_map(create_function('$a', 'return preg_quote($a, \'#\');'), $options['variable_prefixes'])).')';
  56. $options['segment_separators_regex'] = '(?:'.implode('|', array_map(create_function('$a', 'return preg_quote($a, \'#\');'), $options['segment_separators'])).')';
  57. $options['variable_content_regex'] = '[^'.implode('', array_map(create_function('$a', 'return str_replace(\'-\', \'\-\', preg_quote($a, \'#\'));'), $options['segment_separators'])).']+';
  58. if (!isset($options['load_configuration']))
  59. {
  60. $options['load_configuration'] = false;
  61. }
  62. $this->setDefaultSuffix(isset($options['suffix']) ? $options['suffix'] : '');
  63. parent::initialize($dispatcher, $cache, $options);
  64. if (!is_null($this->cache) && $cacheData = $this->cache->get('symfony.routing.data'))
  65. {
  66. $this->cacheData = unserialize($cacheData);
  67. }
  68. }
  69. /**
  70. * @see sfRouting
  71. */
  72. public function loadConfiguration()
  73. {
  74. if (!is_null($this->cache) && $routes = $this->cache->get('symfony.routing.configuration'))
  75. {
  76. $this->routes = unserialize($routes);
  77. }
  78. else
  79. {
  80. if ($this->options['load_configuration'] && $config = sfContext::getInstance()->getConfigCache()->checkConfig('config/routing.yml', true))
  81. {
  82. include($config);
  83. }
  84. parent::loadConfiguration();
  85. if (!is_null($this->cache))
  86. {
  87. $this->cache->set('symfony.routing.configuration', serialize($this->routes));
  88. }
  89. }
  90. }
  91. /**
  92. * @see sfRouting
  93. */
  94. public function getCurrentInternalUri($withRouteName = false)
  95. {
  96. if (is_null($this->currentRouteName))
  97. {
  98. return null;
  99. }
  100. $typeId = $withRouteName ? 0 : 1;
  101. if (!isset($this->currentInternalUri[$typeId]))
  102. {
  103. $parameters = $this->currentRouteParameters;
  104. list($url, $regex, $variables, $defaults, $requirements) = $this->routes[$this->currentRouteName];
  105. $internalUri = $withRouteName ? '@'.$this->currentRouteName : $parameters['module'].'/'.$parameters['action'];
  106. $params = array();
  107. // add parameters
  108. foreach (array_keys(array_merge($defaults, $variables)) as $variable)
  109. {
  110. if ($variable == 'module' || $variable == 'action')
  111. {
  112. continue;
  113. }
  114. $params[] = $variable.'='.(isset($parameters[$variable]) ? $parameters[$variable] : (isset($defaults[$variable]) ? $defaults[$variable] : ''));
  115. }
  116. // add * parameters if needed
  117. if (false !== strpos($regex, '_star'))
  118. {
  119. foreach ($parameters as $key => $value)
  120. {
  121. if ($key == 'module' || $key == 'action' || isset($variables[$key]))
  122. {
  123. continue;
  124. }
  125. $params[] = $key.'='.$value;
  126. }
  127. }
  128. // sort to guaranty unicity
  129. sort($params);
  130. $this->currentInternalUri[$typeId] = $internalUri.($params ? '?'.implode('&', $params) : '');
  131. }
  132. return $this->currentInternalUri[$typeId];
  133. }
  134. /**
  135. * Gets the current route name.
  136. *
  137. * @return string The route name
  138. */
  139. public function getCurrentRouteName()
  140. {
  141. return $this->currentRouteName;
  142. }
  143. /**
  144. * Sets the default suffix
  145. *
  146. * @param string The default suffix
  147. */
  148. public function setDefaultSuffix($suffix)
  149. {
  150. $this->defaultSuffix = '.' == $suffix ? '' : $suffix;
  151. }
  152. /**
  153. * @see sfRouting
  154. */
  155. public function getRoutes()
  156. {
  157. return $this->routes;
  158. }
  159. /**
  160. * @see sfRouting
  161. */
  162. public function setRoutes($routes)
  163. {
  164. return $this->routes = $routes;
  165. }
  166. /**
  167. * @see sfRouting
  168. */
  169. public function hasRoutes()
  170. {
  171. return count($this->routes) ? true : false;
  172. }
  173. /**
  174. * @see sfRouting
  175. */
  176. public function clearRoutes()
  177. {
  178. if ($this->options['logging'])
  179. {
  180. $this->dispatcher->notify(new sfEvent($this, 'application.log', array('Clear all current routes')));
  181. }
  182. $this->routes = array();
  183. }
  184. /**
  185. * Returns true if the route name given is defined.
  186. *
  187. * @param string $name The route name
  188. *
  189. * @return boolean
  190. */
  191. public function hasRouteName($name)
  192. {
  193. return isset($this->routes[$name]) ? true : false;
  194. }
  195. /**
  196. * Adds a new route at the beginning of the current list of routes.
  197. *
  198. * @see connect
  199. */
  200. public function prependRoute($name, $route, $default = array(), $requirements = array())
  201. {
  202. $routes = $this->routes;
  203. $this->routes = array();
  204. $newroutes = $this->connect($name, $route, $default, $requirements);
  205. $this->routes = array_merge($newroutes, $routes);
  206. return $this->routes;
  207. }
  208. /**
  209. * Adds a new route.
  210. *
  211. * Alias for the connect method.
  212. *
  213. * @see connect
  214. */
  215. public function appendRoute($name, $route, $default = array(), $requirements = array())
  216. {
  217. return $this->connect($name, $route, $default, $requirements);
  218. }
  219. /**
  220. * Adds a new route before a given one in the current list of routes.
  221. *
  222. * @see connect
  223. */
  224. public function insertRouteBefore($pivot, $name, $route, $default = array(), $requirements = array())
  225. {
  226. if (!isset($this->routes[$pivot]))
  227. {
  228. throw new sfConfigurationException(sprintf('Unable to insert route "%s" before inexistent route "%s".', $name, $pivot));
  229. }
  230. $routes = $this->routes;
  231. $this->routes = array();
  232. $newroutes = array();
  233. foreach ($routes as $key => $value)
  234. {
  235. if ($key == $pivot)
  236. {
  237. $newroutes = array_merge($newroutes, $this->connect($name, $route, $default, $requirements));
  238. }
  239. $newroutes[$key] = $value;
  240. }
  241. return $this->routes = $newroutes;
  242. }
  243. /**
  244. * Adds a new route at the end of the current list of routes.
  245. *
  246. * A route string is a string with 2 special constructions:
  247. * - :string: :string denotes a named paramater (available later as $request->getParameter('string'))
  248. * - *: * match an indefinite number of parameters in a route
  249. *
  250. * Here is a very common rule in a symfony project:
  251. *
  252. * <code>
  253. * $r->connect('default', '/:module/:action/*');
  254. * </code>
  255. *
  256. * @param string $name The route name
  257. * @param string $route The route string
  258. * @param array $defaults The default parameter values
  259. * @param array $requirements The regexps parameters must match
  260. *
  261. * @return array current routes
  262. */
  263. public function connect($name, $route, $defaults = array(), $requirements = array())
  264. {
  265. // route already exists?
  266. if (isset($this->routes[$name]))
  267. {
  268. throw new sfConfigurationException(sprintf('This named route already exists ("%s").', $name));
  269. }
  270. $suffix = $this->defaultSuffix;
  271. $route = trim($route);
  272. // fix defaults
  273. foreach ($defaults as $key => $value)
  274. {
  275. if (ctype_digit($key))
  276. {
  277. $defaults[$value] = true;
  278. }
  279. else
  280. {
  281. $defaults[$key] = urldecode($value);
  282. }
  283. }
  284. $givenDefaults = $defaults;
  285. $defaults = $this->fixDefaults($defaults);
  286. // fix requirements regexs
  287. foreach ($requirements as $key => $regex)
  288. {
  289. if ('^' == $regex[0])
  290. {
  291. $regex = substr($regex, 1);
  292. }
  293. if ('$' == substr($regex, -1))
  294. {
  295. $regex = substr($regex, 0, -1);
  296. }
  297. $requirements[$key] = $regex;
  298. }
  299. // a route can start by a slash. remove it for parsing.
  300. if (!empty($route) && '/' == $route[0])
  301. {
  302. $route = substr($route, 1);
  303. }
  304. if ($route == '')
  305. {
  306. $this->routes[$name] = array('/', '/^\/*$/', array(), $defaults, $requirements);
  307. }
  308. else
  309. {
  310. // ignore the default suffix if one is already provided in the route
  311. if ('/' == $route[strlen($route) - 1])
  312. {
  313. // route ends by / (directory)
  314. $suffix = '';
  315. }
  316. else if ('.' == $route[strlen($route) - 1])
  317. {
  318. // route ends by . (no suffix)
  319. $suffix = '';
  320. $route = substr($route, 0, strlen($route) -1);
  321. }
  322. else if (preg_match('#\.(?:'.$this->options['variable_prefix_regex'].$this->options['variable_regex'].'|'.$this->options['variable_content_regex'].')$#i', $route))
  323. {
  324. // specific suffix for this route
  325. // a . with a variable after or some cars without any separators
  326. $suffix = '';
  327. }
  328. // parse the route
  329. $segments = array();
  330. $firstOptional = 0;
  331. $buffer = $route;
  332. $afterASeparator = true;
  333. $currentSeparator = '';
  334. $variables = array();
  335. // a route is an array of (separator + variable) or (separator + text) segments
  336. while (strlen($buffer))
  337. {
  338. if ($afterASeparator && preg_match('#^'.$this->options['variable_prefix_regex'].'('.$this->options['variable_regex'].')#', $buffer, $match))
  339. {
  340. // a variable (like :foo)
  341. $variable = $match[1];
  342. if (!isset($requirements[$variable]))
  343. {
  344. $requirements[$variable] = $this->options['variable_content_regex'];
  345. }
  346. $segments[] = $currentSeparator.'(?P<'.$variable.'>'.$requirements[$variable].')';
  347. $currentSeparator = '';
  348. // for 1.0 BC, we don't take into account the default module and action variable
  349. // for 1.2, remove the $givenDefaults var and move the $firstOptional setting to
  350. // the condition below
  351. if (!isset($givenDefaults[$variable]))
  352. {
  353. $firstOptional = count($segments);
  354. }
  355. if (!isset($defaults[$variable]))
  356. {
  357. $defaults[$variable] = null;
  358. }
  359. $buffer = substr($buffer, strlen($match[0]));
  360. $variables[$variable] = $match[0];
  361. $afterASeparator = false;
  362. }
  363. else if ($afterASeparator)
  364. {
  365. // a static text
  366. if (!preg_match('#^(.+?)(?:'.$this->options['segment_separators_regex'].'|$)#', $buffer, $match))
  367. {
  368. throw new InvalidArgumentException(sprintf('Unable to parse "%s" route near "%s".', $route, $buffer));
  369. }
  370. if ('*' == $match[1])
  371. {
  372. $segments[] = '(?:'.$currentSeparator.'(?P<_star>.*))?';
  373. }
  374. else
  375. {
  376. $segments[] = $currentSeparator.preg_quote($match[1], '#');
  377. $firstOptional = count($segments);
  378. }
  379. $currentSeparator = '';
  380. $buffer = substr($buffer, strlen($match[1]));
  381. $afterASeparator = false;
  382. }
  383. else if (preg_match('#^'.$this->options['segment_separators_regex'].'#', $buffer, $match))
  384. {
  385. // a separator (like / or .)
  386. $currentSeparator = preg_quote($match[0], '#');
  387. $buffer = substr($buffer, strlen($match[0]));
  388. $afterASeparator = true;
  389. }
  390. else
  391. {
  392. // parsing problem
  393. throw new InvalidArgumentException(sprintf('Unable to parse "%s" route near "%s".', $route, $buffer));
  394. }
  395. }
  396. // all segments after the last static segment are optional
  397. // be careful, the n-1 is optional only if n is empty
  398. for ($i = $firstOptional, $max = count($segments); $i < $max; $i++)
  399. {
  400. $segments[$i] = str_repeat(' ', $i - $firstOptional).'(?:'.$segments[$i];
  401. $segments[] = str_repeat(' ', $max - $i - 1).')?';
  402. }
  403. $regex = "#^/\n".implode("\n", $segments)."\n".$currentSeparator.preg_quote($suffix, '#')."$#x";
  404. $this->routes[$name] = array('/'.$route.$suffix, $regex, $variables, $defaults, $requirements);
  405. }
  406. if ($this->options['logging'])
  407. {
  408. $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Connect "/%s"%s', $route, $suffix ? ' ("'.$suffix.'" suffix)' : ''))));
  409. }
  410. return $this->routes;
  411. }
  412. /**
  413. * @see sfRouting
  414. */
  415. public function generate($name, $params = array(), $querydiv = '/', $divider = '/', $equals = '/')
  416. {
  417. $params = $this->fixDefaults($params);
  418. if (!is_null($this->cache))
  419. {
  420. $cacheKey = 'generate_'.$name.serialize(array_merge($this->defaultParameters, $params));
  421. if (isset($this->cacheData[$cacheKey]))
  422. {
  423. return $this->cacheData[$cacheKey];
  424. }
  425. }
  426. // named route?
  427. if ($name)
  428. {
  429. if (!isset($this->routes[$name]))
  430. {
  431. throw new sfConfigurationException(sprintf('The route "%s" does not exist.', $name));
  432. }
  433. list($url, $regex, $variables, $defaults, $requirements) = $this->routes[$name];
  434. $defaults = $this->mergeArrays($defaults, $this->defaultParameters);
  435. $tparams = $this->mergeArrays($defaults, $params);
  436. // all params must be given
  437. if ($diff = array_diff_key($variables, array_filter($tparams, create_function('$v', 'return !is_null($v);'))))
  438. {
  439. throw new InvalidArgumentException(sprintf('The "%s" route has some missing mandatory parameters (%s).', $name, implode(', ', $diff)));
  440. }
  441. }
  442. else
  443. {
  444. // find a matching route
  445. $found = false;
  446. foreach ($this->routes as $name => $route)
  447. {
  448. list($url, $regex, $variables, $defaults, $requirements) = $route;
  449. $defaults = $this->mergeArrays($defaults, $this->defaultParameters);
  450. $tparams = $this->mergeArrays($defaults, $params);
  451. // all $variables must be defined in the $tparams array
  452. if (array_diff_key($variables, array_filter($tparams)))
  453. {
  454. continue;
  455. }
  456. // check requirements
  457. foreach ($requirements as $reqParam => $reqRegexp)
  458. {
  459. if (!is_null($tparams[$reqParam]) && !preg_match('#'.$reqRegexp.'#', $tparams[$reqParam]))
  460. {
  461. continue 2;
  462. }
  463. }
  464. // all $params must be in $variables or $defaults if there is no * in route
  465. if (false === strpos($regex, '_star') && array_diff_key(array_filter($params), $variables, $defaults))
  466. {
  467. continue;
  468. }
  469. // check that $params does not override a default value that is not a variable
  470. foreach (array_filter($defaults) as $key => $value)
  471. {
  472. if (!isset($variables[$key]) && $tparams[$key] != $value)
  473. {
  474. continue 2;
  475. }
  476. }
  477. // found
  478. $found = true;
  479. break;
  480. }
  481. if (!$found)
  482. {
  483. throw new sfConfigurationException(sprintf('Unable to find a matching routing rule to generate url for params "%s".', var_export($params, true)));
  484. }
  485. }
  486. // replace variables
  487. $realUrl = $url;
  488. $tmp = $variables;
  489. uasort($tmp, create_function('$a, $b', 'return strlen($a) < strlen($b);'));
  490. foreach ($tmp as $variable => $value)
  491. {
  492. $realUrl = str_replace($value, urlencode($tparams[$variable]), $realUrl);
  493. }
  494. // add extra params if the route contains *
  495. if (false !== strpos($regex, '_star'))
  496. {
  497. $tmp = array();
  498. foreach (array_diff_key($tparams, $variables, $defaults) as $key => $value)
  499. {
  500. if (is_array($value))
  501. {
  502. foreach ($value as $v)
  503. {
  504. $tmp[] = $key.$equals.urlencode($v);
  505. }
  506. }
  507. else
  508. {
  509. $tmp[] = urlencode($key).$equals.urlencode($value);
  510. }
  511. }
  512. $tmp = implode($divider, $tmp);
  513. if ($tmp)
  514. {
  515. $tmp = $querydiv.$tmp;
  516. }
  517. $realUrl = preg_replace('#'.$this->options['segment_separators_regex'].'\*('.$this->options['segment_separators_regex'].'|$)#', "$tmp$1", $realUrl);
  518. }
  519. if (!is_null($this->cache))
  520. {
  521. $this->cacheChanged = true;
  522. $this->cacheData[$cacheKey] = $realUrl;
  523. }
  524. return $realUrl;
  525. }
  526. /**
  527. * @see sfRouting
  528. */
  529. public function parse($url)
  530. {
  531. if (null !== $routeInfo = $this->findRoute($url))
  532. {
  533. // store the route name
  534. $this->currentRouteName = $routeInfo['name'];
  535. $this->currentRouteParameters = $routeInfo['parameters'];
  536. $this->currentInternalUri = array();
  537. if ($this->options['logging'])
  538. {
  539. $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Match route [%s] for "%s"', $routeInfo['name'], $routeInfo['route']))));
  540. }
  541. }
  542. else
  543. {
  544. throw new sfError404Exception(sprintf('No matching route found for "%s"', $url));
  545. }
  546. return $this->currentRouteParameters;
  547. }
  548. /**
  549. * Finds a matching route for given URL.
  550. *
  551. * Returned array contains:
  552. * - name : name or alias of the route that matched
  553. * - route : the actual matching route
  554. * - parameters : array containing key value pairs of the request parameters including defaults
  555. *
  556. * @param string $url URL to be parsed
  557. *
  558. * @return array An array with routing information or null if no route matched
  559. */
  560. public function findRoute($url)
  561. {
  562. // an URL should start with a '/', mod_rewrite doesn't respect that, but no-mod_rewrite version does.
  563. if ('/' != substr($url, 0, 1))
  564. {
  565. $url = '/'.$url;
  566. }
  567. // we remove the query string
  568. if (false !== $pos = strpos($url, '?'))
  569. {
  570. $url = substr($url, 0, $pos);
  571. }
  572. // remove multiple /
  573. $url = preg_replace('#/+#', '/', $url);
  574. if (!is_null($this->cache))
  575. {
  576. $cacheKey = 'parse_'.$url;
  577. if (isset($this->cacheData[$cacheKey]))
  578. {
  579. return $this->cacheData[$cacheKey];
  580. }
  581. }
  582. $routeInfo = null;
  583. foreach ($this->routes as $routeName => $route)
  584. {
  585. list($route, $regex, $variables, $defaults, $requirements) = $route;
  586. if (!preg_match($regex, $url, $r))
  587. {
  588. continue;
  589. }
  590. $defaults = array_merge($defaults, $this->defaultParameters);
  591. $out = array();
  592. // *
  593. if (isset($r['_star']))
  594. {
  595. $out = $this->parseStarParameter($r['_star']);
  596. unset($r['_star']);
  597. }
  598. // defaults
  599. $out = $this->mergeArrays($out, $defaults);
  600. // variables
  601. foreach ($r as $key => $value)
  602. {
  603. if (!is_int($key))
  604. {
  605. $out[$key] = urldecode($value);
  606. }
  607. }
  608. $routeInfo['name'] = $routeName;
  609. $routeInfo['route'] = $route;
  610. $routeInfo['parameters'] = $this->fixDefaults($out);
  611. if (!is_null($this->cache))
  612. {
  613. $this->cacheChanged = true;
  614. $this->cacheData[$cacheKey] = $routeInfo;
  615. }
  616. break;
  617. }
  618. return $routeInfo;
  619. }
  620. protected function parseStarParameter($star)
  621. {
  622. $parameters = array();
  623. $tmp = explode('/', $star);
  624. for ($i = 0, $max = count($tmp); $i < $max; $i += 2)
  625. {
  626. //dont allow a param name to be empty - #4173
  627. if (!empty($tmp[$i]))
  628. {
  629. $parameters[$tmp[$i]] = isset($tmp[$i + 1]) ? urldecode($tmp[$i + 1]) : true;
  630. }
  631. }
  632. return $parameters;
  633. }
  634. /**
  635. * @see sfRouting
  636. */
  637. public function shutdown()
  638. {
  639. if (!is_null($this->cache) && $this->cacheChanged)
  640. {
  641. $this->cacheChanged = false;
  642. $this->cache->set('symfony.routing.data', serialize($this->cacheData));
  643. }
  644. }
  645. }