Development

/branches/1.1/lib/routing/sfPatternRouting.class.php

You must first sign up to be able to contribute.

root/branches/1.1/lib/routing/sfPatternRouting.class.php

Revision 17746, 20.3 kB (checked in by fabien, 5 years ago)

[1.1, 1.2, 1.3] fixed a warning

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