Development

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

You must first sign up to be able to contribute.

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

Revision 32939, 22.1 kB (checked in by fabien, 3 years ago)

[1.4] fixed memory leak in sfRoute::generate() (closes #9886 - patch from acf0)

  • 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, array('sfRoute', 'generateCompareVarsByStrlen'));
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   static private function generateCompareVarsByStrlen($a, $b)
263   {
264     return strlen($a) < strlen($b);
265   }
266
267   /**
268    * Generates a URL for the given parameters by using the route tokens.
269    *
270    * @param array $parameters An array of parameters
271    */
272   protected function generateWithTokens($parameters)
273   {
274     $url = array();
275     $optional = $this->options['generate_shortest_url'];
276     $first = true;
277     $tokens = array_reverse($this->tokens);
278     foreach ($tokens as $token)
279     {
280       switch ($token[0])
281       {
282         case 'variable':
283           if (!$optional || !isset($this->defaults[$token[3]]) || $parameters[$token[3]] != $this->defaults[$token[3]])
284           {
285             $url[] = urlencode($parameters[$token[3]]);
286             $optional = false;
287           }
288           break;
289         case 'text':
290           $url[] = $token[2];
291           $optional = false;
292           break;
293         case 'separator':
294           if (false === $optional || $first)
295           {
296             $url[] = $token[2];
297           }
298           break;
299         default:
300           // handle custom tokens
301           if ($segment = call_user_func_array(array($this, 'generateFor'.ucfirst(array_shift($token))), array_merge(array($optional, $parameters), $token)))
302           {
303             $url[] = $segment;
304             $optional = false;
305           }
306           break;
307       }
308
309       $first = false;
310     }
311
312     $url = implode('', array_reverse($url));
313     if (!$url)
314     {
315       $url = '/';
316     }
317
318     return $url;
319   }
320
321   /**
322    * Returns the route parameters.
323    *
324    * @return array The route parameters
325    */
326   public function getParameters()
327   {
328     if (!$this->compiled)
329     {
330       $this->compile();
331     }
332
333     return $this->parameters;
334   }
335
336   /**
337    * Returns the compiled pattern.
338    *
339    * @return string The compiled pattern
340    */
341   public function getPattern()
342   {
343     if (!$this->compiled)
344     {
345       $this->compile();
346     }
347
348     return $this->pattern;
349   }
350
351   /**
352    * Returns the compiled regex.
353    *
354    * @return string The compiled regex
355    */
356   public function getRegex()
357   {
358     if (!$this->compiled)
359     {
360       $this->compile();
361     }
362
363     return $this->regex;
364   }
365
366   /**
367    * Returns the compiled tokens.
368    *
369    * @return array The compiled tokens
370    */
371   public function getTokens()
372   {
373     if (!$this->compiled)
374     {
375       $this->compile();
376     }
377
378     return $this->tokens;
379   }
380
381   /**
382    * Returns the compiled options.
383    *
384    * @return array The compiled options
385    */
386   public function getOptions()
387   {
388     if (!$this->compiled)
389     {
390       $this->compile();
391     }
392
393     return $this->options;
394   }
395
396   /**
397    * Returns the compiled variables.
398    *
399    * @return array The compiled variables
400    */
401   public function getVariables()
402   {
403     if (!$this->compiled)
404     {
405       $this->compile();
406     }
407
408     return $this->variables;
409   }
410
411   /**
412    * Returns the compiled defaults.
413    *
414    * @return array The compiled defaults
415    */
416   public function getDefaults()
417   {
418     if (!$this->compiled)
419     {
420       $this->compile();
421     }
422
423     return $this->defaults;
424   }
425
426   /**
427    * Returns the compiled requirements.
428    *
429    * @return array The compiled requirements
430    */
431   public function getRequirements()
432   {
433     if (!$this->compiled)
434     {
435       $this->compile();
436     }
437
438     return $this->requirements;
439   }
440
441   /**
442    * Compiles the current route instance.
443    */
444   public function compile()
445   {
446     if ($this->compiled)
447     {
448       return;
449     }
450
451     $this->initializeOptions();
452     $this->fixRequirements();
453     $this->fixDefaults();
454     $this->fixSuffix();
455
456     $this->compiled = true;
457     $this->firstOptional = 0;
458     $this->segments = array();
459
460     $this->preCompile();
461
462     $this->tokenize();
463
464     // parse
465     foreach ($this->tokens as $token)
466     {
467       call_user_func_array(array($this, 'compileFor'.ucfirst(array_shift($token))), $token);
468     }
469
470     $this->postCompile();
471
472     $separator = '';
473     if (count($this->tokens))
474     {
475       $lastToken = $this->tokens[count($this->tokens) - 1];
476       $separator = 'separator' == $lastToken[0] ? $lastToken[2] : '';
477     }
478
479     $this->regex = "#^".implode("", $this->segments)."".preg_quote($separator, '#')."$#x";
480   }
481
482   /**
483    * Pre-compiles a route.
484    */
485   protected function preCompile()
486   {
487     // a route must start with a slash
488     if (empty($this->pattern) || '/' != $this->pattern[0])
489     {
490       $this->pattern = '/'.$this->pattern;
491     }
492   }
493
494   /**
495    * Post-compiles a route.
496    */
497   protected function postCompile()
498   {
499     // all segments after the last static segment are optional
500     // be careful, the n-1 is optional only if n is empty
501     for ($i = $this->firstOptional, $max = count($this->segments); $i < $max; $i++)
502     {
503       $this->segments[$i] = (0 == $i ? '/?' : '').str_repeat(' ', $i - $this->firstOptional).'(?:'.$this->segments[$i];
504       $this->segments[] = str_repeat(' ', $max - $i - 1).')?';
505     }
506
507     $this->staticPrefix = '';
508     foreach ($this->tokens as $token)
509     {
510       switch ($token[0])
511       {
512         case 'separator':
513           break;
514         case 'text':
515           if ($token[2] !== '*')
516           {
517             // non-star text is static
518             $this->staticPrefix .= $token[1].$token[2];
519             break;
520           }
521         default:
522           // everything else indicates variable parts. break switch and for loop
523           break 2;
524       }
525     }
526   }
527
528   /**
529    * Tokenizes the route.
530    */
531   protected function tokenize()
532   {
533     $this->tokens = array();
534     $buffer = $this->pattern;
535     $afterASeparator = false;
536     $currentSeparator = '';
537
538     // a route is an array of (separator + variable) or (separator + text) segments
539     while (strlen($buffer))
540     {
541       if (false !== $this->tokenizeBufferBefore($buffer, $this->tokens, $afterASeparator, $currentSeparator))
542       {
543         // a custom token
544         $this->customToken = true;
545       }
546       else if ($afterASeparator && preg_match('#^'.$this->options['variable_prefix_regex'].'('.$this->options['variable_regex'].')#', $buffer, $match))
547       {
548         // a variable
549         $this->tokens[] = array('variable', $currentSeparator, $match[0], $match[1]);
550
551         $currentSeparator = '';
552         $buffer = substr($buffer, strlen($match[0]));
553         $afterASeparator = false;
554       }
555       else if ($afterASeparator && preg_match('#^('.$this->options['text_regex'].')(?:'.$this->options['segment_separators_regex'].'|$)#', $buffer, $match))
556       {
557         // a text
558         $this->tokens[] = array('text', $currentSeparator, $match[1], null);
559
560         $currentSeparator = '';
561         $buffer = substr($buffer, strlen($match[1]));
562         $afterASeparator = false;
563       }
564       else if (!$afterASeparator && preg_match('#^/|^'.$this->options['segment_separators_regex'].'#', $buffer, $match))
565       {
566         // beginning of URL (^/) or a separator
567         $this->tokens[] = array('separator', $currentSeparator, $match[0], null);
568
569         $currentSeparator = $match[0];
570         $buffer = substr($buffer, strlen($match[0]));
571         $afterASeparator = true;
572       }
573       else if (false !== $this->tokenizeBufferAfter($buffer, $this->tokens, $afterASeparator, $currentSeparator))
574       {
575         // a custom token
576         $this->customToken = true;
577       }
578       else
579       {
580         // parsing problem
581         throw new InvalidArgumentException(sprintf('Unable to parse "%s" route near "%s".', $this->pattern, $buffer));
582       }
583     }
584     
585     // check for suffix
586     if ($this->suffix)
587     {
588       // treat as a separator
589       $this->tokens[] = array('separator', $currentSeparator, $this->suffix);
590     }
591
592   }
593
594   /**
595    * Tokenizes the buffer before default logic is applied.
596    *
597    * This method must return false if the buffer has not been parsed.
598    *
599    * @param string   $buffer           The current route buffer
600    * @param array    $tokens           An array of current tokens
601    * @param Boolean  $afterASeparator  Whether the buffer is just after a separator
602    * @param string   $currentSeparator The last matched separator
603    *
604    * @return Boolean true if a token has been generated, false otherwise
605    */
606   protected function tokenizeBufferBefore(&$buffer, &$tokens, &$afterASeparator, &$currentSeparator)
607   {
608     return false;
609   }
610
611   /**
612    * Tokenizes the buffer after default logic is applied.
613    *
614    * This method must return false if the buffer has not been parsed.
615    *
616    * @param string   $buffer           The current route buffer
617    * @param array    $tokens           An array of current tokens
618    * @param Boolean  $afterASeparator  Whether the buffer is just after a separator
619    * @param string   $currentSeparator The last matched separator
620    *
621    * @return Boolean true if a token has been generated, false otherwise
622    */
623   protected function tokenizeBufferAfter(&$buffer, &$tokens, &$afterASeparator, &$currentSeparator)
624   {
625     return false;
626   }
627
628   protected function compileForText($separator, $text)
629   {
630     if ('*' == $text)
631     {
632       $this->segments[] = '(?:'.preg_quote($separator, '#').'(?P<_star>.*))?';
633     }
634     else
635     {
636       $this->firstOptional = count($this->segments) + 1;
637
638       $this->segments[] = preg_quote($separator, '#').preg_quote($text, '#');
639     }
640   }
641
642   protected function compileForVariable($separator, $name, $variable)
643   {
644     if (!isset($this->requirements[$variable]))
645     {
646       $this->requirements[$variable] = $this->options['variable_content_regex'];
647     }
648
649     $this->segments[] = preg_quote($separator, '#').'(?P<'.$variable.'>'.$this->requirements[$variable].')';
650     $this->variables[$variable] = $name;
651
652     if (!isset($this->defaults[$variable]))
653     {
654       $this->firstOptional = count($this->segments);
655     }
656   }
657
658   protected function compileForSeparator($separator, $regexSeparator)
659   {
660   }
661
662   public function getDefaultParameters()
663   {
664     return $this->defaultParameters;
665   }
666
667   public function setDefaultParameters($parameters)
668   {
669     $this->defaultParameters = $parameters;
670   }
671
672   public function getDefaultOptions()
673   {
674     return $this->defaultOptions;
675   }
676
677   public function setDefaultOptions($options)
678   {
679     $this->defaultOptions = $options;
680   }
681
682   protected function initializeOptions()
683   {
684     $this->options = array_merge(array(
685       'suffix'                           => '',
686       'variable_prefixes'                => array(':'),
687       'segment_separators'               => array('/', '.'),
688       'variable_regex'                   => '[\w\d_]+',
689       'text_regex'                       => '.+?',
690       'generate_shortest_url'            => true,
691       'extra_parameters_as_query_string' => true,
692     ), $this->getDefaultOptions(), $this->options);
693
694     $preg_quote_hash = create_function('$a', 'return preg_quote($a, \'#\');');
695
696     // compute some regexes
697     $this->options['variable_prefix_regex'] = '(?:'.implode('|', array_map($preg_quote_hash, $this->options['variable_prefixes'])).')';
698
699     if (count($this->options['segment_separators']))
700     {
701       $this->options['segment_separators_regex'] = '(?:'.implode('|', array_map($preg_quote_hash, $this->options['segment_separators'])).')';
702
703       // as of PHP 5.3.0, preg_quote automatically quotes dashes "-" (see http://bugs.php.net/bug.php?id=47229)
704       $preg_quote_hash_53 = create_function('$a', 'return str_replace(\'-\', \'\-\', preg_quote($a, \'#\'));');
705       $this->options['variable_content_regex'] = '[^'.implode('',
706           array_map(version_compare(PHP_VERSION, '5.3.0RC4', '>=') ? $preg_quote_hash : $preg_quote_hash_53, $this->options['segment_separators'])
707         ).']+';
708     }
709     else
710     {
711       // use simplified regexes for case where no separators are used
712       $this->options['segment_separators_regex'] = '()';
713       $this->options['variable_content_regex']   = '.+';
714     }
715   }
716
717   protected function parseStarParameter($star)
718   {
719     $parameters = array();
720     $tmp = explode('/', $star);
721     for ($i = 0, $max = count($tmp); $i < $max; $i += 2)
722     {
723       //dont allow a param name to be empty - #4173
724       if (!empty($tmp[$i]))
725       {
726         $parameters[$tmp[$i]] = isset($tmp[$i + 1]) ? urldecode($tmp[$i + 1]) : true;
727       }
728     }
729
730     return $parameters;
731   }
732
733   protected function hasStarParameter()
734   {
735     return false !== strpos($this->regex, '<_star>');
736   }
737
738   protected function generateStarParameter($url, $defaults, $parameters)
739   {
740     if (false === strpos($this->regex, '<_star>'))
741     {
742       return $url;
743     }
744
745     $tmp = array();
746     foreach (array_diff_key($parameters, $this->variables, $defaults) as $key => $value)
747     {
748       if (is_array($value))
749       {
750         foreach ($value as $v)
751         {
752           $tmp[] = $key.'='.urlencode($v);
753         }
754       }
755       else
756       {
757         $tmp[] = urlencode($key).'/'.urlencode($value);
758       }
759     }
760     $tmp = implode('/', $tmp);
761     if ($tmp)
762     {
763       $tmp = '/'.$tmp;
764     }
765
766     return preg_replace('#'.$this->options['segment_separators_regex'].'\*('.$this->options['segment_separators_regex'].'|$)#', "$tmp$1", $url);
767   }
768
769   protected function mergeArrays($arr1, $arr2)
770   {
771     foreach ($arr2 as $key => $value)
772     {
773       $arr1[$key] = $value;
774     }
775
776     return $arr1;
777   }
778
779   protected function fixDefaults()
780   {
781     foreach ($this->defaults as $key => $value)
782     {
783       if (ctype_digit($key))
784       {
785         $this->defaults[$value] = true;
786       }
787       else
788       {
789         $this->defaults[$key] = urldecode($value);
790       }
791     }
792   }
793
794   protected function fixRequirements()
795   {
796     foreach ($this->requirements as $key => $regex)
797     {
798       if (!is_string($regex))
799       {
800         continue;
801       }
802
803       if ('^' == $regex[0])
804       {
805         $regex = substr($regex, 1);
806       }
807       if ('$' == substr($regex, -1))
808       {
809         $regex = substr($regex, 0, -1);
810       }
811
812       $this->requirements[$key] = $regex;
813     }
814   }
815
816   protected function fixSuffix()
817   {
818     $length = strlen($this->pattern);
819
820     if ($length > 0 && '/' == $this->pattern[$length - 1])
821     {
822       // route ends by / (directory)
823       $this->suffix = '/';
824     }
825     else if ($length > 0 && '.' == $this->pattern[$length - 1])
826     {
827       // route ends by . (no suffix)
828       $this->suffix = '';
829       $this->pattern = substr($this->pattern, 0, $length - 1);
830     }
831     else if (preg_match('#\.(?:'.$this->options['variable_prefix_regex'].$this->options['variable_regex'].'|'.$this->options['variable_content_regex'].')$#i', $this->pattern))
832     {
833       // specific suffix for this route
834       // a . with a variable after or some chars without any separators
835       $this->suffix = '';
836     }
837     else
838     {
839       $this->suffix = $this->options['suffix'];
840     }
841   }
842
843   public function serialize()
844   {
845     // always serialize compiled routes
846     $this->compile();
847     // sfPatternRouting will always re-set defaultParameters, so no need to serialize them
848     return serialize(array($this->tokens, $this->defaultOptions, $this->options, $this->pattern, $this->staticPrefix, $this->regex, $this->variables, $this->defaults, $this->requirements, $this->suffix, $this->customToken));
849   }
850
851   public function unserialize($data)
852   {
853     list($this->tokens, $this->defaultOptions, $this->options, $this->pattern, $this->staticPrefix, $this->regex, $this->variables, $this->defaults, $this->requirements, $this->suffix, $this->customToken) = unserialize($data);
854     $this->compiled = true;
855   }
856 }
Note: See TracBrowser for help on using the browser.