Development

/branches/1.3/lib/routing/sfRoute.class.php

You must first sign up to be able to contribute.

root/branches/1.3/lib/routing/sfRoute.class.php

Revision 31398, 22.0 kB (checked in by fabien, 3 years ago)

[1.3, 1.4] fixed typo in sfRoute class (closes #9255, patch from Pavel.Campr)

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