Development

Changeset 7525

You must first sign up to be able to contribute.

Changeset 7525

Show
Ignore:
Timestamp:
02/17/08 11:30:23 (9 months ago)
Author:
fabien
Message:

refactored sfPatternRouting

  • everything is BC
  • rewrote the connect(), parse() and generate() algorithm from scratch
    • the route parsing in connect() is much more robust now and extensible
    • removed all hacks related to the suffix and the first/last /
    • implemented sensible rules for optional segments
      • the last variable is optional
      • the n-1 variable is only optional if the n variable is optional
      • a static segment is not optional
      • a * segment is not optional
    • the generated regexes are easier to debug (indentation and embedded variable names)
    • an exception is thrown if the route is not parsable
    • all variables have specific requirements based on the current configuration (see below)
    • parse() and generate() have similar behaviors (defaults, requirements, ...)
    • parse() and generate() must be faster for typical cases (more optimizations to come)
    • parse() returns all variables in the array (null if the variable is optional and not provided)
  • added more configurability to the system
    • the variable prefix is configurable via the variable_prefixes option (: by default)
    • the route segment separator is configurable via the segment_separators option (/ or . by default)
    • the acceptable character set for a variable is configurable via the variable_regex option ([\w\d_]+ by default)
    • so, routes like /foo/:bar.:format are now possible out of the box if you have the latest .htaccess
  • merged unit tests suite from the routing/ branch (francois)
  • added unit tests for some edge cases

You have to clear your project caches after upgrading because the internal format of the routes has changed.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • branches/1.1/lib/routing/sfNoRouting.class.php

    r7518 r7525  
    2424  public function getCurrentInternalUri($with_route_name = false) 
    2525  { 
    26     $parameters = $this->fixDefaults(array_merge($this->defaultParameters, $_GET)); 
     26    $parameters = $this->fixDefaults($this->mergeArrays($this->defaultParameters, $_GET)); 
    2727    $action = sprintf('%s/%s', $parameters['module'], $parameters['action']); 
    2828 
     
    4040  public function generate($name, $params, $querydiv = '/', $divider = '/', $equals = '/') 
    4141  { 
    42     $parameters = http_build_query(array_merge($this->defaultParameters, $params), null, '&'); 
     42    $parameters = http_build_query($this->mergeArrays($this->defaultParameters, $params), null, '&'); 
    4343 
    4444    return '/'.($parameters ? '?'.$parameters : ''); 
  • branches/1.1/lib/routing/sfPathInfoRouting.class.php

    r7518 r7525  
    4343  { 
    4444    $url = ''; 
    45     foreach (array_merge($this->defaultParameters, $params) as $key => $value) 
     45    foreach ($this->mergeArrays($this->defaultParameters, $params) as $key => $value) 
    4646    { 
    4747      $url .= '/'.$key.'/'.$value; 
  • branches/1.1/lib/routing/sfPatternRouting.class.php

    r7518 r7525  
    33/* 
    44 * This file is part of the symfony package. 
    5  * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com> 
     5 * (c) Fabien Potencier <fabien.potencier@symfony-project.com> 
    66 * 
    77 * For the full copyright and license information, please view the LICENSE 
     
    1313 * 
    1414 * It maps an array of parameters to URLs definition. Each map is called a route. 
    15  * 
    16  * This class was initialy based on the Routes class of the Cake framework. 
    1715 * 
    1816 * @package    symfony 
     
    3129 
    3230  /** 
     31   * Initializes this Routing. 
     32   * 
     33   * Available options: 
     34   * 
     35   *  * suffix:             The default suffix 
     36   *  * variable_prefixes:  An array of characters that starts a variable name (: by default) 
     37   *  * segment_separators: An array of allowed characters for segment separators (/ and . by default) 
     38   *  * variable_regex:     A regex that match a valid variable name ([\w\d_]+ by default) 
     39   * 
    3340   * @see sfRouting 
    3441   */ 
    3542  public function initialize(sfEventDispatcher $dispatcher, $options = array()) 
    3643  { 
     44    if (!isset($options['variable_prefixes'])) 
     45    { 
     46      $options['variable_prefixes'] = array(':'); 
     47    } 
     48 
     49    if (!isset($options['segment_separators'])) 
     50    { 
     51      $options['segment_separators'] = array('/', '.'); 
     52    } 
     53 
     54    if (!isset($options['variable_regex'])) 
     55    { 
     56      $options['variable_regex'] = '[\w\d_]+'; 
     57    } 
     58 
     59    $options['variable_prefix_regex']    = '(?:'.implode('|', array_map(create_function('$a', 'return preg_quote($a, \'#\');'), $options['variable_prefixes'])).')'; 
     60    $options['segment_separators_regex'] = '(?:'.implode('|', array_map(create_function('$a', 'return preg_quote($a, \'#\');'), $options['segment_separators'])).')'; 
     61    $options['variable_content_regex']   = '[^'.implode('', array_map(create_function('$a', 'return str_replace(\'-\', \'\-\', preg_quote($a, \'#\'));'), $options['segment_separators'])).']+'; 
     62 
    3763    parent::initialize($dispatcher, $options); 
    3864 
     
    6995      $parameters = $this->currentRouteParameters; 
    7096 
    71       list($url, $regexp, $names, $namesHash, $defaults, $requirements, $suffix) = $this->routes[$this->currentRouteName]; 
     97      list($url, $regex, $variables, $defaults, $requirements) = $this->routes[$this->currentRouteName]; 
    7298 
    7399      $internalUri = $withRouteName ? '@'.$this->currentRouteName : $parameters['module'].'/'.$parameters['action']; 
     
    76102 
    77103      // add parameters 
    78       foreach ($names as $name) 
    79       { 
    80         if ($name == 'module' || $name == 'action') 
     104      foreach (array_keys($variables) as $variable) 
     105      { 
     106        if ($variable == 'module' || $variable == 'action') 
    81107        { 
    82108          continue; 
    83109        } 
    84110 
    85         $params[] = $name.'='.(isset($parameters[$name]) ? $parameters[$name] : (isset($defaults[$name]) ? $defaults[$name] : '')); 
     111        $params[] = $variable.'='.(isset($parameters[$variable]) ? $parameters[$variable] : (isset($defaults[$variable]) ? $defaults[$variable] : '')); 
    86112      } 
    87113 
    88114      // add * parameters if needed 
    89       if (strpos($url, '*')) 
     115      if (false !== strpos($regex, '_star')) 
    90116      { 
    91117        foreach ($parameters as $key => $value) 
    92118        { 
    93           if ($key == 'module' || $key == 'action' || in_array($key, $names)) 
     119          if ($key == 'module' || $key == 'action' || isset($variables[$key])) 
    94120          { 
    95121            continue; 
     
    205231   * 
    206232   * <code> 
    207    * $r->connect('/:module/:action/*'); 
     233   * $r->connect('default', '/:module/:action/*'); 
    208234   * </code> 
    209235   * 
     
    215241   * @return array  current routes 
    216242   */ 
    217   public function connect($name, $route, $default = array(), $requirements = array()) 
     243  public function connect($name, $route, $defaults = array(), $requirements = array()) 
    218244  { 
    219245    // route already exists? 
     
    223249    } 
    224250 
    225     $parsed = array(); 
    226     $names  = array(); 
    227251    $suffix = $this->defaultSuffix; 
    228  
    229     // a route must start by a slash. If there is none, add it automatically 
    230     if ('/' != $route[0]) 
    231     { 
    232       $route = '/'.$route;  
    233     } 
    234  
    235     if ($route == '/') 
    236     { 
    237       $this->routes[$name] = array($route, '/^[\/]*$/', array(), array(), $default, $requirements, $suffix); 
     252    $route  = trim($route); 
     253 
     254    // fix defaults 
     255    foreach ($defaults as $key => $value) 
     256    { 
     257      if (ctype_digit($key)) 
     258      { 
     259        $defaults[$value] = true; 
     260      } 
     261      else 
     262      { 
     263        $defaults[$key] = urldecode($value); 
     264      } 
     265    } 
     266    $defaults = $this->fixDefaults($defaults); 
     267 
     268    // fix requirements regexs 
     269    foreach ($requirements as $key => $regex) 
     270    { 
     271      if ('^' == $regex[0]) 
     272      { 
     273        $regex = substr($regex, 1); 
     274      } 
     275      if ('$' == substr($regex, -1)) 
     276      { 
     277        $regex = substr($regex, 0, -1); 
     278      } 
     279 
     280      $requirements[$key] = $regex; 
     281    } 
     282 
     283    // a route can start by a slash. remove it for parsing. 
     284    if (!empty($route) && '/' == $route[0]) 
     285    { 
     286      $route = substr($route, 1);  
     287    } 
     288 
     289    if ($route == '') 
     290    { 
     291      $this->routes[$name] = array('/', '/^\/*$/', array(), $defaults, $requirements); 
    238292    } 
    239293    else 
    240294    { 
    241       // used for performance reasons 
    242       $namesHash = array(); 
    243       $r = null; 
    244       $elements = array(); 
    245       foreach (explode('/', $route) as $element) 
    246       { 
    247         if (trim($element)) 
    248         { 
    249           $elements[] = $element; 
    250         } 
    251       } 
    252  
    253       if (!isset($elements[0])) 
    254       { 
    255         return false; 
    256       } 
    257  
    258       // specific suffix for this route? 
    259       // or /$ directory 
    260       if (preg_match('/^(.+)(\.\w*)$/i', $elements[count($elements) - 1], $matches)) 
    261       { 
    262         $suffix = '.' == $matches[2] ? '' : $matches[2]; 
    263         $elements[count($elements) - 1] = $matches[1]; 
    264         $route = '/'.implode('/', $elements); 
    265       } 
    266       else if ($route{strlen($route) - 1} == '/') 
    267       { 
    268         $suffix = '/'; 
    269       } 
    270  
    271       $regexp_suffix = preg_quote($suffix); 
    272  
    273       foreach ($elements as $element) 
    274       { 
    275         if (preg_match('/^:(.+)$/', $element, $r)) 
    276         { 
    277           $element = $r[1]; 
    278  
    279           // regex is [^\/]+ or the requirement regex 
    280           if (isset($requirements[$element])) 
    281           { 
    282             $regex = $requirements[$element]; 
    283             if (0 === strpos($regex, '^')) 
    284             { 
    285               $regex = substr($regex, 1); 
    286             } 
    287             if (strlen($regex) - 1 === strpos($regex, '$')) 
    288             { 
    289               $regex = substr($regex, 0, -1); 
    290             } 
     295      // ignore the default suffix if one is already provided in the route 
     296      if ('/' == $route[strlen($route) - 1]) 
     297      { 
     298        // route ends by / (directory) 
     299        $suffix = ''; 
     300      } 
     301      else if ('.' == $route[strlen($route) - 1]) 
     302      { 
     303        // route ends by . (no suffix) 
     304        $suffix = ''; 
     305        $route = substr($route, 0, strlen($route) -1);  
     306      } 
     307      else if (preg_match('#\.(?:'.$this->options['variable_prefix_regex'].$this->options['variable_regex'].'|'.$this->options['variable_content_regex'].')$#i', $route)) 
     308      { 
     309        // specific suffix for this route 
     310        // a . with a variable after or some cars without any separators 
     311        $suffix = ''; 
     312      } 
     313 
     314      // parse the route 
     315      $segments = array(); 
     316      $firstOptional = 0; 
     317      $buffer = $route; 
     318      $afterASeparator = true; 
     319      $currentSeparator = ''; 
     320      $variables = array(); 
     321 
     322      // a route is an array of (separator + variable) or (separator + text) segments 
     323      while (strlen($buffer)) 
     324      { 
     325        if ($afterASeparator && preg_match('#^'.$this->options['variable_prefix_regex'].'('.$this->options['variable_regex'].')#', $buffer, $match)) 
     326        { 
     327          // a variable (like :foo) 
     328          $variable = $match[1]; 
     329 
     330          if (!isset($requirements[$variable])) 
     331          { 
     332            $requirements[$variable] = $this->options['variable_content_regex']; 
     333          } 
     334 
     335          $segments[] = $currentSeparator.'(?P<'.$variable.'>'.$requirements[$variable].')'; 
     336          $currentSeparator = ''; 
     337 
     338          if (!isset($defaults[$variable])) 
     339          { 
     340            $defaults[$variable] = null; 
     341          } 
     342 
     343          $buffer = substr($buffer, strlen($match[0])); 
     344          $variables[$variable] = $match[0]; 
     345          $afterASeparator = false; 
     346        } 
     347        else if ($afterASeparator) 
     348        { 
     349          // a static text 
     350          if (!preg_match('#^(.+?)(?:'.$this->options['segment_separators_regex'].'|$)#', $buffer, $match)) 
     351          { 
     352            throw new InvalidArgumentException(sprintf('Unable to parse "%s" route near "%s".', $route, $buffer)); 
     353          } 
     354 
     355          if ('*' == $match[1]) 
     356          { 
     357            $segments[] = '(?:'.$currentSeparator.'(?P<_star>.*))?'; 
    291358          } 
    292359          else 
    293360          { 
    294             $regex = '[^\/]+'; 
    295           } 
    296  
    297           $parsed[] = '(?:\/('.$regex.'))?'; 
    298           $names[] = $element; 
    299           $namesHash[$element] = 1; 
    300         } 
    301         elseif (preg_match('/^\*$/', $element, $r)) 
    302         { 
    303           $parsed[] = '(?:\/(.*))?'; 
     361            $segments[] = $currentSeparator.preg_quote($match[1], '#'); 
     362            $firstOptional = count($segments); 
     363          } 
     364          $currentSeparator = ''; 
     365 
     366          $buffer = substr($buffer, strlen($match[1])); 
     367          $afterASeparator = false; 
     368        } 
     369        else if (preg_match('#^'.$this->options['segment_separators_regex'].'#', $buffer, $match)) 
     370        { 
     371          // a separator (like / or .) 
     372          $currentSeparator = preg_quote($match[0], '#'); 
     373 
     374          $buffer = substr($buffer, strlen($match[0])); 
     375          $afterASeparator = true; 
    304376        } 
    305377        else 
    306378        { 
    307           $parsed[] = '/'.$element; 
    308         } 
    309       } 
    310       $regexp = '#^'.join('', $parsed).$regexp_suffix.'$#'; 
    311  
    312       $this->routes[$name] = array($route, $regexp, $names, $namesHash, $default, $requirements, $suffix); 
     379          // parsing problem 
     380          throw new InvalidArgumentException(sprintf('Unable to parse "%s" route near "%s".', $route, $buffer)); 
     381        } 
     382      } 
     383 
     384      // all segments after the last static segment are optional 
     385      // be careful, the n-1 is optional only if n is empty 
     386      for ($i = $firstOptional, $max = count($segments); $i < $max; $i++) 
     387      { 
     388        $segments[$i] = str_repeat(' ', $i - $firstOptional).'(?:'.$segments[$i]; 
     389        $segments[] = str_repeat(' ', $max - $i - 1).')?'; 
     390      } 
     391 
     392      $regex = "#^/\n".implode("\n", $segments)."\n".$currentSeparator.preg_quote($suffix, '#')."$#x"; 
     393      $this->routes[$name] = array('/'.$route.$suffix, $regex, $variables, $defaults, $requirements); 
    313394    } 
    314395 
     
    336417      } 
    337418 
    338       list($url, $regexp, $names, $namesHash, $defaults, $requirements, $suffix) = $this->routes[$name]; 
    339       $defaults = array_merge($defaults, $this->defaultParameters); 
     419      list($url, $regex, $variables, $defaults, $requirements) = $this->routes[$name]; 
     420      $defaults = $this->mergeArrays($defaults, $this->defaultParameters); 
     421      $tparams = $this->mergeArrays($defaults, $params); 
    340422 
    341423      // all params must be given 
    342       foreach ($names as $tmp) 
    343       { 
    344         if (!isset($params[$tmp]) && !isset($defaults[$tmp])) 
    345         { 
    346           throw new sfException(sprintf('Route named "%s" have a mandatory "%s" parameter.', $name, $tmp)); 
    347         } 
     424      if ($diff = array_diff_key($variables, array_filter($tparams))) 
     425      { 
     426        throw new InvalidArgumentException(sprintf('The "%s" route has some missing mandatory parameters (%s).', $name, implode(', ', $diff))); 
    348427      } 
    349428    } 
     
    354433      foreach ($this->routes as $name => $route) 
    355434      { 
    356         list($url, $regexp, $names, $namesHash, $defaults, $requirements, $suffix) = $route; 
    357         $defaults = array_merge($defaults, $this->defaultParameters); 
    358  
    359         $tparams = array_merge($defaults, $params); 
    360  
    361         // we must match all names (all $names keys must be in $params array) 
    362         foreach ($names as $key) 
    363         { 
    364           if (!isset($tparams[$key])) continue 2; 
    365         } 
    366  
    367         // we must match all defaults with value except if present in names 
    368         foreach ($defaults as $key => $value) 
    369         { 
    370           if (isset($namesHash[$key])) continue; 
    371  
    372           if (!isset($tparams[$key]) || $tparams[$key] != $value) continue 2; 
    373         } 
    374  
    375         // we must match all requirements for rule 
    376         foreach ($requirements as $req_param => $req_regexp) 
    377         { 
    378           if (!preg_match('/'.str_replace('/', '\\/', $req_regexp).'/', $tparams[$req_param])) 
     435        list($url, $regex, $variables, $defaults, $requirements) = $route; 
     436        $defaults = $this->mergeArrays($defaults, $this->defaultParameters); 
     437        $tparams = $this->mergeArrays($defaults, $params); 
     438 
     439        // all $variables must be defined in the $tparams array 
     440        if (array_diff_key($variables, array_filter($tparams))) 
     441        { 
     442          continue; 
     443        } 
     444 
     445        // check requirements 
     446        foreach ($requirements as $reqParam => $reqRegexp) 
     447        { 
     448          if (!is_null($tparams[$reqParam]) && !preg_match('#'.$reqRegexp.'#', $tparams[$reqParam])) 
    379449          { 
    380450            continue 2; 
     
    382452        } 
    383453 
    384         // we must have consumed all $params keys if there is no * in route 
    385         if (!strpos($url, '*')) 
    386         { 
    387           if (count(array_diff(array_keys($tparams), $names, array_keys($defaults)))) 
    388           { 
    389             continue; 
    390           } 
    391         } 
    392  
    393         // match found 
     454        // all $params must be in $variables or $defaults if there is no * in route 
     455        if (false === strpos($regex, '_star') && array_diff_key(array_filter($params), $variables, $defaults)) 
     456        { 
     457          continue; 
     458        } 
     459 
     460        // check that $params does not override a default value that is not a variable 
     461        foreach (array_filter($defaults) as $key => $value) 
     462        { 
     463          if (!isset($variables[$key]) && $tparams[$key] != $value) 
     464          { 
     465            continue 2; 
     466          } 
     467        } 
     468 
     469        // found 
    394470        $found = true; 
    395471        break; 
     
    402478    } 
    403479 
    404     $params = sfToolkit::arrayDeepMerge($defaults, $params); 
    405  
    406     $realUrl = preg_replace('/\:([^\/]+)/e', 'urlencode($params["\\1"])', $url); 
    407  
    408     // we add all other params if * 
    409     if (strpos($realUrl, '*')) 
     480    // replace variables 
     481    $realUrl = $url; 
     482    foreach ($variables as $variable => $value) 
     483    { 
     484      $realUrl = str_replace($value, urlencode($tparams[$variable]), $realUrl); 
     485    } 
     486 
     487    // add extra params if the route contains * 
     488    if (false !== strpos($regex, '_star')) 
    410489    { 
    411490      $tmp = array(); 
    412       foreach ($params as $key => $value) 
    413       { 
    414         if (isset($namesHash[$key]) || isset($defaults[$key])) continue; 
    415  
     491      foreach (array_diff_key($tparams, $variables, $defaults) as $key => $value) 
     492      { 
    416493        if (is_array($value)) 
    417494        { 
     
    427504      } 
    428505      $tmp = implode($divider, $tmp); 
    429       if (strlen($tmp) > 0
     506      if ($tmp
    430507      { 
    431508        $tmp = $querydiv.$tmp; 
    432509      } 
    433       $realUrl = preg_replace('/\/\*(\/|$)/', "$tmp$1", $realUrl); 
    434  
    435       // strip off last divider character 
    436       if (strlen($realUrl) > 1) 
    437       { 
    438         $realUrl = rtrim($realUrl, $divider); 
    439       } 
    440     } 
    441  
    442     if ('/' != $realUrl && '/' != substr($realUrl, -1)) 
    443     { 
    444       $realUrl .= $suffix; 
     510 
     511      $realUrl = preg_replace('#'.$this->options['segment_separators_regex'].'\*('.$this->options['segment_separators_regex'].'|$)#', "$tmp$1", $realUrl); 
    445512    } 
    446513 
     
    454521  { 
    455522    // an URL should start with a '/', mod_rewrite doesn't respect that, but no-mod_rewrite version does. 
    456     if ($url && ('/' != $url[0])
     523    if ('/' != $url[0]
    457524    { 
    458525      $url = '/'.$url; 
     
    460527 
    461528    // we remove the query string 
    462     if ($pos = strpos($url, '?')) 
     529    if (false !== $pos = strpos($url, '?')) 
    463530    { 
    464531      $url = substr($url, 0, $pos); 
    465532    } 
    466533 
    467     // we remove multiple / 
     534    // remove multiple / 
    468535    $url = preg_replace('#/+#', '/', $url); 
    469     $out = array(); 
    470     $break = false; 
     536 
     537    $found = false; 
    471538    foreach ($this->routes as $routeName => $route) 
    472539    { 
    473       $out = array(); 
    474       $r = null; 
    475  
    476       list($route, $regexp, $names, $namesHash, $defaults, $requirements, $suffix) = $route; 
     540      list($route, $regex, $variables, $defaults, $requirements) = $route; 
     541      if (!preg_match($regex, $url, $r)) 
     542      { 
     543        continue; 
     544      } 
     545 
    477546      $defaults = array_merge($defaults, $this->defaultParameters); 
    478  
    479       $break = false; 
    480  
    481       if (preg_match($regexp, $url, $r)) 
    482       { 
    483         $break = true; 
    484  
    485         // remove the first element, which is the url 
    486         array_shift($r); 
    487  
    488         // hack, pre-fill the default route names 
    489         foreach ($names as $name) 
    490         { 
    491           $out[$name] = null; 
    492         } 
    493  
    494         // defaults 
    495         foreach ($defaults as $name => $value) 
    496         { 
    497           if (preg_match('#[a-z_\-]#i', $name)) 
    498           { 
    499             $out[$name] = urldecode($value); 
    500           } 
    501           else 
    502           { 
    503             $out[$value] = true; 
    504           } 
    505         } 
    506  
    507         $pos = 0; 
    508         foreach ($r as $found) 
    509         { 
    510           // if $found is a named url element (i.e. ':action') 
    511           if (isset($names[$pos])) 
    512           { 
    513             $out[$names[$pos]] = urldecode($found); 
    514           } 
    515           // unnamed elements go in as 'pass' 
    516           else 
    517           { 
    518             $pass = explode('/', $found); 
    519             $found = ''; 
    520             for ($i = 0, $max = count($pass); $i < $max; $i += 2) 
    521             { 
    522               if (!isset($pass[$i + 1])) 
    523               { 
    524                 continue; 
    525               } 
    526  
    527               $found .= $pass[$i].'='.$pass[$i + 1].'&'; 
    528             } 
    529             parse_str($found, $pass); 
    530  
    531             if (get_magic_quotes_gpc()) 
    532             { 
    533               $pass = sfToolkit::stripslashesDeep((array) $pass); 
    534             } 
    535  
    536             foreach ($pass as $key => $value) 
    537             { 
    538               // we add this parameters if not in conflict with named url element (i.e. ':action') 
    539               if (!isset($namesHash[$key])) 
    540               { 
    541                 $out[$key] = $value; 
    542               } 
    543             } 
    544           } 
    545           $pos++; 
    546         } 
    547  
    548         // we must have found all :var stuffs in url? except if default values exists 
    549         foreach ($names as $name) 
    550         { 
    551           if (is_null($out[$name])) 
    552           { 
    553             $break = false; 
    554           } 
    555         } 
    556  
    557         if ($break) 
    558         { 
    559           // we store route name 
    560           $this->currentRouteName = $routeName; 
    561           $this->currentInternalUri = array(); 
    562  
    563           if ($this->options['logging']) 
    564           { 
    565             $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Match route [%s] "%s"', $routeName, $route)))); 
    566           } 
    567  
    568           break; 
    569         } 
    570       } 
     547      $found    = true; 
     548      $out      = array(); 
     549 
     550      // * 
     551      if (isset($r['_star'])) 
     552      { 
     553        $out = $this->parseStarParameter($r['_star']); 
     554        unset($r['_star']); 
     555      } 
     556 
     557      // defaults 
     558      $out = $this->mergeArrays($out, $defaults); 
     559 
     560      // variables 
     561      foreach ($r as $key => $value) 
     562      { 
     563        if (!is_int($key)) 
     564        { 
     565          $out[$key] = $value; 
     566        } 
     567      } 
     568 
     569      // store the route name 
     570      $this->currentRouteName = $routeName; 
     571      $this->currentInternalUri = array(); 
     572 
     573      if ($this->options['logging']) 
     574      { 
     575        $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Match route [%s] for "%s"', $routeName, $route)))); 
     576      } 
     577 
     578      break; 
    571579    } 
    572580 
    573581    // no route found 
    574     if (!$break) 
    575     { 
    576       if ($this->options['logging']) 
    577       { 
    578         $this->dispatcher->notify(new sfEvent($this, 'application.log', array('No matching route found'))); 
    579       } 
    580  
    581       $this->currentRouteParameters = null; 
    582  
     582    if (!$found) 
     583    { 
    583584      throw new sfError404Exception(sprintf('No matching route found for "%s"', $url)); 
    584585    } 
    585586 
    586     $this->currentRouteParameters = $this->fixDefaults($out); 
    587  
    588     return $this->currentRouteParameters; 
     587    return $this->currentRouteParameters = $this->fixDefaults($out); 
     588  } 
     589 
     590  protected function parseStarParameter($star) 
     591  { 
     592    $parameters = array(); 
     593    $tmp = explode('/', $star); 
     594    for ($i = 0, $max = count($tmp); $i < $max; $i += 2) 
     595    { 
     596      $parameters[$tmp[$i]] = isset($tmp[$i + 1]) ? urldecode($tmp[$i + 1]) : true; 
     597    } 
     598 
     599    return $parameters; 
    589600  } 
    590601} 
  • branches/1.1/lib/routing/sfRouting.class.php

    r7518 r7525  
    180180  } 
    181181 
     182  protected function mergeArrays($arr1, $arr2) 
     183  { 
     184    foreach ($arr2 as $key => $value) 
     185    { 
     186      $arr1[$key] = $value; 
     187    } 
     188 
     189    return $arr1; 
     190  } 
     191 
    182192  /** 
    183193   * Listens to the user.change_culture event. 
  • branches/1.1/test/unit/routing/sfPatternRoutingTest.php

    r7519 r7525  
    1111require_once(dirname(__FILE__).'/../../bootstrap/unit.php'); 
    1212 
    13 $t = new lime_test(74, new lime_output_color()); 
     13$t = new lime_test(122, new lime_output_color()); 
    1414 
    1515class sfPatternRoutingTest extends sfPatternRouting 
     
    2222 
    2323// public methods 
    24 $r = new sfPatternRoutingTest(new sfEventDispatcher(), array('default_module' => 'default', 'default_action' => 'index')); 
    25 foreach (array('clearRoutes', 'connect', 'generate', 'getCurrentInternalUri', 'getCurrentRouteName', 'getRoutes', 'hasRoutes', 'parse', 'setRoutes') as $method) 
     24$r = new sfPatternRoutingTest(new sfEventDispatcher()); 
     25foreach (array('initialize', 'getCurrentInternalUri', 'getRoutes', 'setRoutes', 'hasRoutes', 'clearRoutes', 'hasRouteName', 'prependRoute', 'appendRoute', 'connect', 'generate', 'parse') as $method) 
    2626{ 
    2727  $t->can_ok($r, $method, sprintf('"%s" is a method of sfRouting', $method)); 
     
    6363$t->is($r->hasRoutes(), true, '->hasRoutes() returns true if some routes are registered'); 
    6464 
    65 // ->connect(), ->parse(), ->generate() 
    66 $t->diag('->connect(), ->parse(), ->generate()'); 
     65// ->connect() 
     66$t->diag('->connect()'); 
     67$r->clearRoutes(); 
     68$msg = '->connect() throws an sfConfigurationException when a route already exists with same name'; 
     69$r->connect('test', '/index.php/:module/:action', array('module' => 'default', 'action' => 'index')); 
     70try 
     71
     72  $r->connect('test', '/index.php/:module/:action', array('module' => 'default', 'action' => 'index')); 
     73 
     74  $t->fail($msg); 
     75
     76catch (sfConfigurationException $e) 
     77
     78  $t->pass($msg); 
     79
     80$r->clearRoutes(); 
     81$routes = $r->connect('test', ':module/:action', array('module' => 'default', 'action' => 'index')); 
     82$t->is($routes['test'][0], '/:module/:action', '->connect() automatically adds trailing / to route if missing'); 
     83$routes = $r->connect('test1', '', array('module' => 'default', 'action' => 'index')); 
     84$t->is($routes['test1'][1], '/^\/*$/', '->connect() detects empty routes'); 
     85$routes = $r->connect('test2', '/', array('module' => 'default', 'action' => 'index')); 
     86$t->is($routes['test1'][1], '/^\/*$/', '->connect() detects empty routes'); 
     87 
     88// route syntax 
     89$t->diag('route syntax'); 
    6790 
    6891// simple routes 
     
    78101$t->is($r->generate('', $params), $url, 'generate /:module/:action url'); 
    79102 
     103// order 
     104$t->diag('route order'); 
     105$r->clearRoutes(); 
     106$r->connect('test', '/test/:id', array('module' => 'default1', 'action' => 'index1'), array('id' => '\d+')); 
     107$r->connect('test1', '/test/:id', array('module' => 'default2', 'action' => 'index2')); 
     108$params = array('module' => 'default1', 'action' => 'index1', 'id' => '12'); 
     109$url = '/test/12'; 
     110$t->is($r->parse($url), $params, '->parse()    takes the first matching route'); 
     111$t->is($r->generate('', $params), $url, '->generate() takes the first matching route'); 
     112 
     113$params = array('module' => 'default2', 'action' => 'index2', 'id' => 'foo'); 
     114$url = '/test/foo'; 
     115$t->is($r->parse($url), $params, '->parse()    takes the first matching route'); 
     116$t->is($r->generate('', $params), $url, '->generate() takes the first matching route'); 
     117 
     118$r->clearRoutes(); 
     119$r->connect('test', '/:module/:action/test/:id/:test', array('module' => 'default', 'action' => 'index')); 
     120$r->connect('test1', '/:module/:action/test/:id', array('module' => 'default', 'action' => 'index', 'id' => 'foo')); 
     121$params = array('module' => 'default', 'action' => 'index', 'id' => 'foo', 'test' => null); 
     122$url = '/default/index/test/foo'; 
     123$t->is($r->parse($url), $params, '->parse()    takes the first matching route'); 
     124$t->is($r->generate('', $params), $url, '->generate() takes the first matching route'); 
     125 
    80126// suffix 
     127$t->diag('suffix'); 
    81128$r->clearRoutes(); 
    82129$r->setDefaultSuffix('.html'); 
    83 $r->connect('foo', '/foo/:module/:action/:param.foo', array('module' => 'default', 'action' => 'index')); 
    84 $url  = '/foo/default/index/foo.foo'; 
     130$r->connect('foo0', '/foo0/:module/:action/:param0', array('module' => 'default', 'action' => 'index0')); 
     131$url0 = '/foo0/default/index0/foo0.html'; 
    85132$r->connect('foo1', '/foo1/:module/:action/:param1.', array('module' => 'default', 'action' => 'index1')); 
    86133$url1 = '/foo1/default/index1/foo1'; 
    87134$r->connect('foo2', '/foo2/:module/:action/:param2/', array('module' => 'default', 'action' => 'index2')); 
    88135$url2 = '/foo2/default/index2/foo2/'; 
    89 $r->connect('foo3', '/foo3/:module/:action/:param3', array('module' => 'default', 'action' => 'index3')); 
    90 $url3 = '/foo3/default/index3/foo3.html'; 
    91  
    92 $t->is($r->generate('', array('module' => 'default', 'action' => 'index',  'param'  => 'foo'),  '/', '/', '='), $url,  '->generate() routes can override the default suffix'); 
    93 $t->is($r->generate('', array('module' => 'default', 'action' => 'index1', 'param1' => 'foo1')), $url1, '->generate() routes can remove the default suffix'); 
    94 $t->is($r->generate('', array('module' => 'default', 'action' => 'index2', 'param2' => 'foo2')), $url2, '->generate() routes does not have suffix when they end by /'); 
    95 $t->is($r->generate('', array('module' => 'default', 'action' => 'index3', 'param3' => 'foo3')), $url3, '->generate() routes takes a suffix defined by the "suffix" parameter'); 
    96  
    97 $t->is($r->parse($url),  array('module' => 'default', 'action' => 'index',  'param'  => 'foo'),  '->parse() routes can override the default suffix'); 
    98 $t->is($r->parse($url1), array('module' => 'default', 'action' => 'index1', 'param1' => 'foo1'), '->parse() routes can remove the default suffix'); 
    99 $t->is($r->parse($url2), array('module' => 'default', 'action' => 'index2', 'param2' => 'foo2'), '->parse() routes does not have suffix when they end by /'); 
    100 $t->is($r->parse($url3), array('module' => 'default', 'action' => 'index3', 'param3' => 'foo3'), '->parse() routes takes a suffix defined by the "suffix" parameter'); 
     136$r->connect('foo3', '/foo3/:module/:action/:param3.foo', array('module' => 'default', 'action' => 'index3')); 
     137$url3 = '/foo3/default/index3/foo3.foo'; 
     138$r->connect('foo4', '/foo4/:module/:action/:param4.:param_5', array('module' => 'default', 'action' => 'index4')); 
     139$url4 = '/foo4/default/index4/foo.bar'; 
     140 
     141$t->is($r->generate('', array('module' => 'default', 'action' => 'index0', 'param0' => 'foo0')), $url0, '->generate() creates URL suffixed by "sf_suffix" parameter'); 
     142$t->is($r->generate('', array('module' => 'default', 'action' => 'index1', 'param1' => 'foo1')), $url1, '->generate() creates URL with no suffix when route ends with .'); 
     143$t->is($r->generate('', array('module' => 'default', 'action' => 'index2', 'param2' => 'foo2')), $url2, '->generate() creates URL with no suffix when route ends with /'); 
     144$t->is($r->generate('', array('module' => 'default', 'action' => 'index3',  'param3'  => 'foo3'),  '/', '/', '='), $url3,  '->generate() creates URL with special suffix when route ends with .suffix'); 
     145$t->is($r->generate('', array('module' => 'default', 'action' => 'index4', 'param4' => 'foo', 'param_5' => 'bar')), $url4, '->generate() creates URL with no special suffix when route ends with .:suffix'); 
     146 
     147 
     148$t->is($r->parse($url0), array('module' => 'default', 'action' => 'index0', 'param0' => 'foo0'), '->parse() finds route from URL suffixed by "sf_suffix"'); 
     149$t->is($r->parse($url1), array('module' => 'default', 'action' => 'index1', 'param1' => 'foo1'), '->parse() finds route with no suffix when route ends with .'); 
     150$t->is($r->parse($url2), array('module' => 'default', 'action' => 'index2', 'param2' => 'foo2'), '->parse() finds route with no suffix when route ends with /'); 
     151$t->is($r->parse($url3),  array('module' => 'default', 'action' => 'index3',  'param3'  => 'foo3'),  '->parse() finds route with special suffix when route ends with .suffix'); 
     152$t->is($r->parse($url4),  array('module' => 'default', 'action' => 'index4',  'param4'  => 'foo', 'param_5' => 'bar'),  '->parse() finds route with special suffix when route ends with .:suffix'); 
     153 
    101154$r->setDefaultSuffix('.'); 
    102155 
    103 // duplicate names 
    104 $msg = '->connect() throws an sfConfigurationException when a route already exists with same name'; 
    105 $r->clearRoutes(); 
    106 $r->connect('test', '/index.php/:module/:action', array('module' => 'default', 'action' => 'index')); 
    107 try 
    108 { 
    109   $r->connect('test', '/index.php/:module/:action', array('module' => 'default', 'action' => 'index')); 
    110  
    111   $t->fail($msg); 
    112 } 
    113 catch (sfConfigurationException $e) 
    114 { 
    115   $t->pass($msg); 
    116 } 
    117  
    118156// query string 
     157$t->diag('query string'); 
    119158$r->clearRoutes(); 
    120159$r->connect('test', '/index.php/:module/:action', array('module' => 'default', 'action' => 'index')); 
     
    124163 
    125164// default values 
     165$t->diag('default values'); 
    126166$r->clearRoutes(); 
    127167$r->connect('test', '/:module/:action', array('module' => 'default', 'action' => 'index')); 
     168$t->is($r->generate('', array('module' => 'default')), '/default/index',  
     169    '->generate() creates URL for route with missing parameter if parameter is set in the default values'); 
     170$t->is($r->parse('/default'), array('module' => 'default', 'action' => 'index'),  
     171    '->parse()    finds route for URL   with missing parameter if parameter is set in the default values'); 
     172 
     173$r->clearRoutes(); 
     174$r->connect('test', '/:module/:action/:foo', array('module' => 'default', 'action' => 'index', 'foo' => 'bar')); 
     175$t->is($r->generate('', array('module' => 'default')), '/default/index/bar',  
     176    '->generate() creates URL for route with more than one missing parameter if default values are set'); 
     177$t->is($r->parse('/default'), array('module' => 'default', 'action' => 'index', 'foo' => 'bar'),  
     178    '->parse()    finds route for URL   with more than one missing parameter if default values are set'); 
     179 
     180$r->clearRoutes(); 
     181$r->connect('test', '/:module/:action', array('module' => 'default', 'action' => 'index')); 
     182$params = array('module' => 'foo', 'action' => 'bar'); 
     183$url = '/foo/bar'; 
     184$t->is($r->generate('', $params), $url, '->generate() parameters override the route default values'); 
     185$t->is($r->parse($url), $params, '->parse()    finds route with parameters distinct from the default values'); 
     186 
     187$r->clearRoutes(); 
     188$r->connect('test', '/:module/:action', array('module' => 'default')); 
    128189$params = array('module' => 'default', 'action' => 'index'); 
    129190$url = '/default/index'; 
    130 $t->is($r->parse($url), $params, '->parse() routes can have default values for its parameters'); 
    131 $t->is($r->generate('', $params), $url, '->generate() routes can have default values for its parameters'); 
    132  
    133 // params 
    134 $r->clearRoutes(); 
    135 $r->connect('test', '/:module/:action/test/:id', array('module' => 'default', 'action' => 'index')); 
    136 $params = array('module' => 'default', 'action' => 'index', 'id' => 4); 
    137 $url = '/default/index/test/4'; 
    138 $t->is($r->parse($url), $params, '->parse() routes can have parameters with no default'); 
    139 $t->is($r->generate('', $params), $url, '->generate() routes can have parameters with no default'); 
    140  
    141 // order 
    142 $r->clearRoutes(); 
    143 $r->connect('test', '/:module/:action/test/:id/:test', array('module' => 'default', 'action' => 'index')); 
    144 $r->connect('test1', '/:module/:action/test/:id', array('module' => 'default', 'action' => 'index', 'id' => 'foo')); 
    145 $params = array('module' => 'default', 'action' => 'index', 'id' => 'foo'); 
    146 $url = '/default/index/test/foo'; 
    147 $t->is($r->parse($url), $params, '->parse() takes the first matching route'); 
    148 $t->is($r->generate('', $params), $url, '->generate() takes the first matching route'); 
    149  
    150 // multiple params 
     191$t->is($r->generate('', $params), $url, '->generate() creates URL even if there is no default value'); 
     192$t->is($r->parse($url), $params, '->parse()    finds route even when route has no default value'); 
     193 
     194// combined examples 
    151195$r->clearRoutes(); 
    152196$r->connect('test', '/:module/:action/:test/:id', array('module' => 'default', 'action' => 'index', 'id' => 'toto')); 
    153197$params = array('module' => 'default', 'action' => 'index', 'test' => 'foo', 'id' => 'bar'); 
    154198$url = '/default/index/foo/bar'; 
    155 $t->is($r->parse($url), $params, '->parse() routes have default parameters value that can be overriden'); 
    156199$t->is($r->generate('', $params), $url, '->generate() routes have default parameters value that can be overriden'); 
     200$t->is($r->parse($url), $params, '->parse()    routes have default parameters value that can be overriden'); 
    157201$params = array('module' => 'default', 'action' => 'index', 'test' => 'foo', 'id' => 'toto'); 
    158202$url = '/default/index/foo'; 
    159 $t->is($r->parse($url), $params, '->parse() removes the last parameter if the parameter is default value'); 
    160 //$t->is($r->generate('', $params), $url, '->generate() removes the last parameter if the parameter is default value'); 
    161  
    162 // numerics params 
    163 $r->clearRoutes(); 
    164 $r->connect('test', '/:module/:action/*', array('module' => 'default', 'action' => 'index')); 
    165 $params = array('module' => 'default', 'action' => 'index', 15 => 'foo', 32 => 'bar', 'foo' => 'bar'); 
    166 $url = '/default/index/15/foo/32/bar/foo/bar'; 
    167 $t->is($r->parse($url), $params, '->parse() routes can have numeric parameters'); 
    168 $t->is($r->generate('', $params), $url, '->generate() routes can have numeric parameters'); 
    169  
     203$t->isnt($r->generate('', $params), $url, '->generate() does not remove the last parameter if the parameter is default value'); 
     204$t->is($r->parse($url), $params, '->parse()    removes the last parameter if the parameter is default value'); 
    170205 
    171206$r->clearRoutes(); 
     
    173208$params = array('module' => 'default', 'action' => 'index', 'test' => 'foo', 'id' => 'bar'); 
    174209$url = '/default/index'; 
    175 $t->is($r->parse($url), $params, '->parse() removes last parameters if they have default values'); 
    176 //$t->is($r->generate('', $params), $url, '->generate() removes last parameters if they have default values'); 
    177  
    178 // star parameter 
     210$t->isnt($r->generate('', $params), $url, '->generate() does not remove last parameters if they have default values'); 
     211$t->is($r->parse($url), $params, '->parse()    removes last parameters if they have default values'); 
     212 
     213// routing defaults parameters 
     214$r->setDefaultParameter('foo', 'bar'); 
     215$r->clearRoutes(); 
     216$r->connect('test', '/test/:foo/:id', array('module' => 'default', 'action' => 'index')); 
     217$params = array('module' => 'default', 'action' => 'index', 'id' => 12); 
     218$url = '/test/bar/12'; 
     219$t->is($r->generate('', $params), $url, '->generate() merges parameters with defaults from "sf_routing_defaults"'); 
     220$r->setDefaultParameters(array()); 
     221 
     222// unnamed wildcard * 
     223$t->diag('unnamed wildcard *'); 
    179224$r->clearRoutes(); 
    180225$r->connect('test', '/:module/:action/test/*', array('module' => 'default', 'action' => 'index')); 
     226$params = array('module' => 'default', 'action' => 'index'); 
     227$url = '/default/index/test'; 
     228$t->is($r->parse($url), $params, '->parse()    finds route for URL   with no additional parameters when route ends with unnamed wildcard *'); 
     229$t->is($r->generate('', $params), $url, '->generate() creates URL for route with no additional parameters when route ends with unnamed wildcard *'); 
    181230$params = array('module' => 'default', 'action' => 'index', 'page' => '4.html', 'toto' => true, 'titi' => 'toto', 'OK' => true); 
    182231$url = '/default/index/test/page/4.html/toto/1/titi/toto/OK/1'; 
    183 $t->is($r->parse($url), $params, '->parse() routes can take a * as its last parameter'); 
    184 $t->is($r->parse('/default/index/test/page/4.html/toto/1/titi/toto/OK/1/module/test/action/tutu'), $params, '->parse() routes can take a * as its last parameter'); 
    185 $t->is($r->parse('/default/index/test/page/4.html////toto//1/titi//toto//OK/1'), $params, '->parse() routes can take a * as its last parameter'); 
    186 $t->is($r->generate('', $params), $url, '->generate() routes can take a * as its last parameter'); 
    187  
     232$t->is($r->parse($url), $params, '->parse()    finds route for URL   with additional parameters when route ends with unnamed wildcard *'); 
     233$t->is($r->generate('', $params), $url, '->generate() creates URL for route with additional parameters when route ends with unnamed wildcard *'); 
     234$t->is($r->parse('/default/index/test/page/4.html/toto/1/titi/toto/OK/1/module/test/action/tutu'), $params, '->parse()    does not override named wildcards with parameters passed in unnamed wildcard *'); 
     235$t->is($r->parse('/default/index/test/page/4.html////toto//1/titi//toto//OK/1'), $params, '->parse()    considers multiple separators as single in unnamed wildcard *'); 
     236 
     237// unnamed wildcard * after a token 
    188238$r->clearRoutes(); 
    189239$r->connect('test',  '/:module', array('action' => 'index')); 
     
    191241$params = array('module' => 'default', 'action' => 'index', 'toto' => 'titi'); 
    192242$url = '/default/index/toto/titi'; 
    193 $t->is($r->parse($url), $params, '->parse() takes the first matching route but takes * into accounts'); 
     243$t->is($r->parse($url), $params, '->parse()    takes the first matching route but takes * into accounts'); 
    194244$t->is($r->generate('', $params), $url, '->generate() takes the first matching route but takes * into accounts'); 
    195245$params = array('module' => 'default', 'action' => 'index'); 
    196246$url = '/default'; 
    197 $t->is($r->parse($url), $params, '->parse() takes the first matching route but takes * into accounts'); 
     247$t->is($r->parse($url), $params, '->parse()