Development

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

You must first sign up to be able to contribute.

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

Revision 21898, 20.7 kB (checked in by fabien, 5 years ago)

[1.2, 1.3] fixed missing variable in route serialization (closes #6786)

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