1. sfRoute.class.php
  2. /** * sfRoute represents a route. * * @package symfony * @subpackage routing * @author Fabien Potencier * @version SVN: $Id: sfRoute.class.php 27183 2010-01-26 11:52:46Z FabianLange $ */
  3. class sfRoute implements Serializable
  4. {
  5. protected
  6. $isBound = false,
  7. $context = null,
  8. $parameters = null,
  9. $suffix = null,
  10. $defaultParameters = array(),
  11. $defaultOptions = array(),
  12. $compiled = false,
  13. $options = array(),
  14. $pattern = null,
  15. $staticPrefix = null,
  16. $regex = null,
  17. $variables = array(),
  18. $defaults = array(),
  19. $requirements = array(),
  20. $tokens = array(),
  21. $customToken = false;
  22. /**
  23. * Constructor.
  24. *
  25. * Available options:
  26. *
  27. * * variable_prefixes: An array of characters that starts a variable name (: by default)
  28. * * segment_separators: An array of allowed characters for segment separators (/ and . by default)
  29. * * variable_regex: A regex that match a valid variable name ([\w\d_]+ by default)
  30. * * generate_shortest_url: Whether to generate the shortest URL possible (true by default)
  31. * * extra_parameters_as_query_string: Whether to generate extra parameters as a query string
  32. *
  33. * @param string $pattern The pattern to match
  34. * @param array $defaults An array of default parameter values
  35. * @param array $requirements An array of requirements for parameters (regexes)
  36. * @param array $options An array of options
  37. */
  38. public function __construct($pattern, array $defaults = array(), array $requirements = array(), array $options = array())
  39. {
  40. $this->pattern = trim($pattern);
  41. $this->defaults = $defaults;
  42. $this->requirements = $requirements;
  43. $this->options = $options;
  44. }
  45. /**
  46. * Binds the current route for a given context and parameters.
  47. *
  48. * @param array $context The context
  49. * @param array $parameters The parameters
  50. */
  51. public function bind($context, $parameters)
  52. {
  53. $this->isBound = true;
  54. $this->context = $context;
  55. $this->parameters = $parameters;
  56. }
  57. /**
  58. * Returns true if the route is bound to context and parameters.
  59. *
  60. * @return Boolean true if theroute is bound to context and parameters, false otherwise
  61. */
  62. public function isBound()
  63. {
  64. return $this->isBound;
  65. }
  66. /**
  67. * Returns an array of parameters if the URL matches this route, false otherwise.
  68. *
  69. * @param string $url The URL
  70. * @param array $context The context
  71. *
  72. * @return array An array of parameters
  73. */
  74. public function matchesUrl($url, $context = array())
  75. {
  76. if (!$this->compiled)
  77. {
  78. $this->compile();
  79. }
  80. // check the static prefix uf the URL first. Only use the more expensive preg_match when it matches
  81. if ('' !== $this->staticPrefix && 0 !== strpos($url, $this->staticPrefix))
  82. {
  83. return false;
  84. }
  85. if (!preg_match($this->regex, $url, $matches))
  86. {
  87. return false;
  88. }
  89. $defaults = array_merge($this->getDefaultParameters(), $this->defaults);
  90. $parameters = array();
  91. // *
  92. if (isset($matches['_star']))
  93. {
  94. $parameters = $this->parseStarParameter($matches['_star']);
  95. unset($matches['_star'], $parameters['module'], $parameters['action']);
  96. }
  97. // defaults
  98. $parameters = $this->mergeArrays($defaults, $parameters);
  99. // variables
  100. foreach ($matches as $key => $value)
  101. {
  102. if (!is_int($key))
  103. {
  104. $parameters[$key] = urldecode($value);
  105. }
  106. }
  107. return $parameters;
  108. }
  109. /**
  110. * Returns true if the parameters matches this route, false otherwise.
  111. *
  112. * @param mixed $params The parameters
  113. * @param array $context The context
  114. *
  115. * @return Boolean true if the parameters matches this route, false otherwise.
  116. */
  117. public function matchesParameters($params, $context = array())
  118. {
  119. if (!$this->compiled)
  120. {
  121. $this->compile();
  122. }
  123. if (!is_array($params))
  124. {
  125. return false;
  126. }
  127. $defaults = $this->mergeArrays($this->getDefaultParameters(), $this->defaults);
  128. $tparams = $this->mergeArrays($defaults, $params);
  129. // all $variables must be defined in the $tparams array
  130. if (array_diff_key($this->variables, $tparams))
  131. {
  132. return false;
  133. }
  134. // check requirements
  135. foreach (array_keys($this->variables) as $variable)
  136. {
  137. if (!$tparams[$variable])
  138. {
  139. continue;
  140. }
  141. if (!preg_match('#'.$this->requirements[$variable].'#', $tparams[$variable]))
  142. {
  143. return false;
  144. }
  145. }
  146. // all $params must be in $variables or $defaults if there is no * in route
  147. if (!$this->options['extra_parameters_as_query_string'])
  148. {
  149. if (false === strpos($this->regex, '<_star>') && array_diff_key($params, $this->variables, $defaults))
  150. {
  151. return false;
  152. }
  153. }
  154. // check that $params does not override a default value that is not a variable
  155. foreach ($defaults as $key => $value)
  156. {
  157. if (!isset($this->variables[$key]) && $tparams[$key] != $value)
  158. {
  159. return false;
  160. }
  161. }
  162. return true;
  163. }
  164. /**
  165. * Generates a URL from the given parameters.
  166. *
  167. * @param mixed $params The parameter values
  168. * @param array $context The context
  169. * @param Boolean $absolute Whether to generate an absolute URL
  170. *
  171. * @return string The generated URL
  172. */
  173. public function generate($params, $context = array(), $absolute = false)
  174. {
  175. if (!$this->compiled)
  176. {
  177. $this->compile();
  178. }
  179. $url = $this->pattern;
  180. $defaults = $this->mergeArrays($this->getDefaultParameters(), $this->defaults);
  181. $tparams = $this->mergeArrays($defaults, $params);
  182. // all params must be given
  183. if ($diff = array_diff_key($this->variables, $tparams))
  184. {
  185. throw new InvalidArgumentException(sprintf('The "%s" route has some missing mandatory parameters (%s).', $this->pattern, implode(', ', $diff)));
  186. }
  187. if ($this->options['generate_shortest_url'] || $this->customToken)
  188. {
  189. $url = $this->generateWithTokens($tparams);
  190. }
  191. else
  192. {
  193. // replace variables
  194. $variables = $this->variables;
  195. uasort($variables, create_function('$a, $b', 'return strlen($a) < strlen($b);'));
  196. foreach ($variables as $variable => $value)
  197. {
  198. $url = str_replace($value, urlencode($tparams[$variable]), $url);
  199. }
  200. if(!in_array($this->suffix, $this->options['segment_separators']))
  201. {
  202. $url .= $this->suffix;
  203. }
  204. }
  205. // replace extra parameters if the route contains *
  206. $url = $this->generateStarParameter($url, $defaults, $tparams);
  207. if ($this->options['extra_parameters_as_query_string'] && !$this->hasStarParameter())
  208. {
  209. // add a query string if needed
  210. if ($extra = array_diff_key($params, $this->variables, $defaults))
  211. {
  212. $url .= '?'.http_build_query($extra);
  213. }
  214. }
  215. return $url;
  216. }
  217. /**
  218. * Generates a URL for the given parameters by using the route tokens.
  219. *
  220. * @param array $parameters An array of parameters
  221. */
  222. protected function generateWithTokens($parameters)
  223. {
  224. $url = array();
  225. $optional = $this->options['generate_shortest_url'];
  226. $first = true;
  227. $tokens = array_reverse($this->tokens);
  228. foreach ($tokens as $token)
  229. {
  230. switch ($token[0])
  231. {
  232. case 'variable':
  233. if (!$optional || !isset($this->defaults[$token[3]]) || $parameters[$token[3]] != $this->defaults[$token[3]])
  234. {
  235. $url[] = urlencode($parameters[$token[3]]);
  236. $optional = false;
  237. }
  238. break;
  239. case 'text':
  240. $url[] = $token[2];
  241. $optional = false;
  242. break;
  243. case 'separator':
  244. if (false === $optional || $first)
  245. {
  246. $url[] = $token[2];
  247. }
  248. break;
  249. default:
  250. // handle custom tokens
  251. if ($segment = call_user_func_array(array($this, 'generateFor'.ucfirst(array_shift($token))), array_merge(array($optional, $parameters), $token)))
  252. {
  253. $url[] = $segment;
  254. $optional = false;
  255. }
  256. break;
  257. }
  258. $first = false;
  259. }
  260. $url = implode('', array_reverse($url));
  261. if (!$url)
  262. {
  263. $url = '/';
  264. }
  265. return $url;
  266. }
  267. /**
  268. * Returns the route parameters.
  269. *
  270. * @return array The route parameters
  271. */
  272. public function getParameters()
  273. {
  274. if (!$this->compiled)
  275. {
  276. $this->compile();
  277. }
  278. return $this->parameters;
  279. }
  280. /**
  281. * Returns the compiled pattern.
  282. *
  283. * @return string The compiled pattern
  284. */
  285. public function getPattern()
  286. {
  287. if (!$this->compiled)
  288. {
  289. $this->compile();
  290. }
  291. return $this->pattern;
  292. }
  293. /**
  294. * Returns the compiled regex.
  295. *
  296. * @return string The compiled regex
  297. */
  298. public function getRegex()
  299. {
  300. if (!$this->compiled)
  301. {
  302. $this->compile();
  303. }
  304. return $this->regex;
  305. }
  306. /**
  307. * Returns the compiled tokens.
  308. *
  309. * @return array The compiled tokens
  310. */
  311. public function getTokens()
  312. {
  313. if (!$this->compiled)
  314. {
  315. $this->compile();
  316. }
  317. return $this->tokens;
  318. }
  319. /**
  320. * Returns the compiled options.
  321. *
  322. * @return array The compiled options
  323. */
  324. public function getOptions()
  325. {
  326. if (!$this->compiled)
  327. {
  328. $this->compile();
  329. }
  330. return $this->options;
  331. }
  332. /**
  333. * Returns the compiled variables.
  334. *
  335. * @return array The compiled variables
  336. */
  337. public function getVariables()
  338. {
  339. if (!$this->compiled)
  340. {
  341. $this->compile();
  342. }
  343. return $this->variables;
  344. }
  345. /**
  346. * Returns the compiled defaults.
  347. *
  348. * @return array The compiled defaults
  349. */
  350. public function getDefaults()
  351. {
  352. if (!$this->compiled)
  353. {
  354. $this->compile();
  355. }
  356. return $this->defaults;
  357. }
  358. /**
  359. * Returns the compiled requirements.
  360. *
  361. * @return array The compiled requirements
  362. */
  363. public function getRequirements()
  364. {
  365. if (!$this->compiled)
  366. {
  367. $this->compile();
  368. }
  369. return $this->requirements;
  370. }
  371. /**
  372. * Compiles the current route instance.
  373. */
  374. public function compile()
  375. {
  376. if ($this->compiled)
  377. {
  378. return;
  379. }
  380. $this->initializeOptions();
  381. $this->fixRequirements();
  382. $this->fixDefaults();
  383. $this->fixSuffix();
  384. $this->compiled = true;
  385. $this->firstOptional = 0;
  386. $this->segments = array();
  387. $this->preCompile();
  388. $this->tokenize();
  389. // parse
  390. foreach ($this->tokens as $token)
  391. {
  392. call_user_func_array(array($this, 'compileFor'.ucfirst(array_shift($token))), $token);
  393. }
  394. $this->postCompile();
  395. $separator = '';
  396. if (count($this->tokens))
  397. {
  398. $lastToken = $this->tokens[count($this->tokens) - 1];
  399. $separator = 'separator' == $lastToken[0] ? $lastToken[2] : '';
  400. }
  401. $this->regex = "#^".implode("", $this->segments)."".preg_quote($separator, '#')."$#x";
  402. }
  403. /**
  404. * Pre-compiles a route.
  405. */
  406. protected function preCompile()
  407. {
  408. // a route must start with a slash
  409. if (empty($this->pattern) || '/' != $this->pattern[0])
  410. {
  411. $this->pattern = '/'.$this->pattern;
  412. }
  413. }
  414. /**
  415. * Post-compiles a route.
  416. */
  417. protected function postCompile()
  418. {
  419. // all segments after the last static segment are optional
  420. // be careful, the n-1 is optional only if n is empty
  421. for ($i = $this->firstOptional, $max = count($this->segments); $i < $max; $i++)
  422. {
  423. $this->segments[$i] = (0 == $i ? '/?' : '').str_repeat(' ', $i - $this->firstOptional).'(?:'.$this->segments[$i];
  424. $this->segments[] = str_repeat(' ', $max - $i - 1).')?';
  425. }
  426. $this->staticPrefix = '';
  427. foreach ($this->tokens as $token)
  428. {
  429. switch ($token[0])
  430. {
  431. case 'separator':
  432. break;
  433. case 'text':
  434. if ($token[2] !== '*')
  435. {
  436. // non-star text is static
  437. $this->staticPrefix .= $token[1].$token[2];
  438. break;
  439. }
  440. default:
  441. // everything else indicates variable parts. break switch and for loop
  442. break 2;
  443. }
  444. }
  445. }
  446. /**
  447. * Tokenizes the route.
  448. */
  449. protected function tokenize()
  450. {
  451. $this->tokens = array();
  452. $buffer = $this->pattern;
  453. $afterASeparator = false;
  454. $currentSeparator = '';
  455. // a route is an array of (separator + variable) or (separator + text) segments
  456. while (strlen($buffer))
  457. {
  458. if (false !== $this->tokenizeBufferBefore($buffer, $tokens, $afterASeparator, $currentSeparator))
  459. {
  460. // a custom token
  461. $this->customToken = true;
  462. }
  463. else if ($afterASeparator && preg_match('#^'.$this->options['variable_prefix_regex'].'('.$this->options['variable_regex'].')#', $buffer, $match))
  464. {
  465. // a variable
  466. $this->tokens[] = array('variable', $currentSeparator, $match[0], $match[1]);
  467. $currentSeparator = '';
  468. $buffer = substr($buffer, strlen($match[0]));
  469. $afterASeparator = false;
  470. }
  471. else if ($afterASeparator && preg_match('#^('.$this->options['text_regex'].')(?:'.$this->options['segment_separators_regex'].'|$)#', $buffer, $match))
  472. {
  473. // a text
  474. $this->tokens[] = array('text', $currentSeparator, $match[1], null);
  475. $currentSeparator = '';
  476. $buffer = substr($buffer, strlen($match[1]));
  477. $afterASeparator = false;
  478. }
  479. else if (!$afterASeparator && preg_match('#^/|^'.$this->options['segment_separators_regex'].'#', $buffer, $match))
  480. {
  481. // beginning of URL (^/) or a separator
  482. $this->tokens[] = array('separator', $currentSeparator, $match[0], null);
  483. $currentSeparator = $match[0];
  484. $buffer = substr($buffer, strlen($match[0]));
  485. $afterASeparator = true;
  486. }
  487. else if (false !== $this->tokenizeBufferAfter($buffer, $tokens, $afterASeparator, $currentSeparator))
  488. {
  489. // a custom token
  490. $this->customToken = true;
  491. }
  492. else
  493. {
  494. // parsing problem
  495. throw new InvalidArgumentException(sprintf('Unable to parse "%s" route near "%s".', $this->pattern, $buffer));
  496. }
  497. }
  498. // check for suffix
  499. if ($this->suffix)
  500. {
  501. // treat as a separator
  502. $this->tokens[] = array('separator', $currentSeparator, $this->suffix);
  503. }
  504. }
  505. /**
  506. * Tokenizes the buffer before default logic is applied.
  507. *
  508. * This method must return false if the buffer has not been parsed.
  509. *
  510. * @param string $buffer The current route buffer
  511. * @param array $tokens An array of current tokens
  512. * @param Boolean $afterASeparator Whether the buffer is just after a separator
  513. * @param string $currentSeparator The last matched separator
  514. *
  515. * @return Boolean true if a token has been generated, false otherwise
  516. */
  517. protected function tokenizeBufferBefore(&$buffer, &$tokens, &$afterASeparator, &$currentSeparator)
  518. {
  519. return false;
  520. }
  521. /**
  522. * Tokenizes the buffer after default logic is applied.
  523. *
  524. * This method must return false if the buffer has not been parsed.
  525. *
  526. * @param string $buffer The current route buffer
  527. * @param array $tokens An array of current tokens
  528. * @param Boolean $afterASeparator Whether the buffer is just after a separator
  529. * @param string $currentSeparator The last matched separator
  530. *
  531. * @return Boolean true if a token has been generated, false otherwise
  532. */
  533. protected function tokenizeBufferAfter(&$buffer, &$tokens, &$afterASeparator, &$currentSeparator)
  534. {
  535. return false;
  536. }
  537. protected function compileForText($separator, $text)
  538. {
  539. if ('*' == $text)
  540. {
  541. $this->segments[] = '(?:'.preg_quote($separator, '#').'(?P<_star>.*))?';
  542. }
  543. else
  544. {
  545. $this->firstOptional = count($this->segments) + 1;
  546. $this->segments[] = preg_quote($separator, '#').preg_quote($text, '#');
  547. }
  548. }
  549. protected function compileForVariable($separator, $name, $variable)
  550. {
  551. if (!isset($this->requirements[$variable]))
  552. {
  553. $this->requirements[$variable] = $this->options['variable_content_regex'];
  554. }
  555. $this->segments[] = preg_quote($separator, '#').'(?P<'.$variable.'>'.$this->requirements[$variable].')';
  556. $this->variables[$variable] = $name;
  557. if (!isset($this->defaults[$variable]))
  558. {
  559. $this->firstOptional = count($this->segments);
  560. }
  561. }
  562. protected function compileForSeparator($separator, $regexSeparator)
  563. {
  564. }
  565. public function getDefaultParameters()
  566. {
  567. return $this->defaultParameters;
  568. }
  569. public function setDefaultParameters($parameters)
  570. {
  571. $this->defaultParameters = $parameters;
  572. }
  573. public function getDefaultOptions()
  574. {
  575. return $this->defaultOptions;
  576. }
  577. public function setDefaultOptions($options)
  578. {
  579. $this->defaultOptions = $options;
  580. }
  581. protected function initializeOptions()
  582. {
  583. $this->options = array_merge(array(
  584. 'suffix' => '',
  585. 'variable_prefixes' => array(':'),
  586. 'segment_separators' => array('/', '.'),
  587. 'variable_regex' => '[\w\d_]+',
  588. 'text_regex' => '.+?',
  589. 'generate_shortest_url' => true,
  590. 'extra_parameters_as_query_string' => true,
  591. ), $this->getDefaultOptions(), $this->options);
  592. $preg_quote_hash = create_function('$a', 'return preg_quote($a, \'#\');');
  593. // compute some regexes
  594. $this->options['variable_prefix_regex'] = '(?:'.implode('|', array_map($preg_quote_hash, $this->options['variable_prefixes'])).')';
  595. if (count($this->options['segment_separators']))
  596. {
  597. $this->options['segment_separators_regex'] = '(?:'.implode('|', array_map($preg_quote_hash, $this->options['segment_separators'])).')';
  598. // as of PHP 5.3.0, preg_quote automatically quotes dashes "-" (see http://bugs.php.net/bug.php?id=47229)
  599. $preg_quote_hash_53 = create_function('$a', 'return str_replace(\'-\', \'\-\', preg_quote($a, \'#\'));');
  600. $this->options['variable_content_regex'] = '[^'.implode('',
  601. array_map(version_compare(PHP_VERSION, '5.3.0RC4', '>=') ? $preg_quote_hash : $preg_quote_hash_53, $this->options['segment_separators'])
  602. ).']+';
  603. }
  604. else
  605. {
  606. // use simplified regexes for case where no separators are used
  607. $this->options['segment_separators_regex'] = '()';
  608. $this->options['variable_content_regex'] = '.+';
  609. }
  610. }
  611. protected function parseStarParameter($star)
  612. {
  613. $parameters = array();
  614. $tmp = explode('/', $star);
  615. for ($i = 0, $max = count($tmp); $i < $max; $i += 2)
  616. {
  617. //dont allow a param name to be empty - #4173
  618. if (!empty($tmp[$i]))
  619. {
  620. $parameters[$tmp[$i]] = isset($tmp[$i + 1]) ? urldecode($tmp[$i + 1]) : true;
  621. }
  622. }
  623. return $parameters;
  624. }
  625. protected function hasStarParameter()
  626. {
  627. return false !== strpos($this->regex, '<_star>');
  628. }
  629. protected function generateStarParameter($url, $defaults, $parameters)
  630. {
  631. if (false === strpos($this->regex, '<_star>'))
  632. {
  633. return $url;
  634. }
  635. $tmp = array();
  636. foreach (array_diff_key($parameters, $this->variables, $defaults) as $key => $value)
  637. {
  638. if (is_array($value))
  639. {
  640. foreach ($value as $v)
  641. {
  642. $tmp[] = $key.'='.urlencode($v);
  643. }
  644. }
  645. else
  646. {
  647. $tmp[] = urlencode($key).'/'.urlencode($value);
  648. }
  649. }
  650. $tmp = implode('/', $tmp);
  651. if ($tmp)
  652. {
  653. $tmp = '/'.$tmp;
  654. }
  655. return preg_replace('#'.$this->options['segment_separators_regex'].'\*('.$this->options['segment_separators_regex'].'|$)#', "$tmp$1", $url);
  656. }
  657. protected function mergeArrays($arr1, $arr2)
  658. {
  659. foreach ($arr2 as $key => $value)
  660. {
  661. $arr1[$key] = $value;
  662. }
  663. return $arr1;
  664. }
  665. protected function fixDefaults()
  666. {
  667. foreach ($this->defaults as $key => $value)
  668. {
  669. if (ctype_digit($key))
  670. {
  671. $this->defaults[$value] = true;
  672. }
  673. else
  674. {
  675. $this->defaults[$key] = urldecode($value);
  676. }
  677. }
  678. }
  679. protected function fixRequirements()
  680. {
  681. foreach ($this->requirements as $key => $regex)
  682. {
  683. if (!is_string($regex))
  684. {
  685. continue;
  686. }
  687. if ('^' == $regex[0])
  688. {
  689. $regex = substr($regex, 1);
  690. }
  691. if ('$' == substr($regex, -1))
  692. {
  693. $regex = substr($regex, 0, -1);
  694. }
  695. $this->requirements[$key] = $regex;
  696. }
  697. }
  698. protected function fixSuffix()
  699. {
  700. $length = strlen($this->pattern);
  701. if ($length > 0 && '/' == $this->pattern[$length - 1])
  702. {
  703. // route ends by / (directory)
  704. $this->suffix = '/';
  705. }
  706. else if ($length > 0 && '.' == $this->pattern[$length - 1])
  707. {
  708. // route ends by . (no suffix)
  709. $this->suffix = '';
  710. $this->pattern = substr($this->pattern, 0, $length - 1);
  711. }
  712. else if (preg_match('#\.(?:'.$this->options['variable_prefix_regex'].$this->options['variable_regex'].'|'.$this->options['variable_content_regex'].')$#i', $this->pattern))
  713. {
  714. // specific suffix for this route
  715. // a . with a variable after or some chars without any separators
  716. $this->suffix = '';
  717. }
  718. else
  719. {
  720. $this->suffix = $this->options['suffix'];
  721. }
  722. }
  723. public function serialize()
  724. {
  725. // always serialize compiled routes
  726. $this->compile();
  727. // sfPatternRouting will always re-set defaultParameters, so no need to serialize them
  728. return serialize(array($this->tokens, $this->defaultOptions, $this->options, $this->pattern, $this->staticPrefix, $this->regex, $this->variables, $this->defaults, $this->requirements, $this->suffix));
  729. }
  730. public function unserialize($data)
  731. {
  732. list($this->tokens, $this->defaultOptions, $this->options, $this->pattern, $this->staticPrefix, $this->regex, $this->variables, $this->defaults, $this->requirements, $this->suffix) = unserialize($data);
  733. $this->compiled = true;
  734. }
  735. }

Debug toolbar