<?php

/**
 * Proof of concept implementation for sfPopulatedRouting based on sfPatternRouting.
 * Ideally should extend it.
 *
 */
class sfPopulatedRouting extends sfPatternRouting
{
  
/**
   * Adds a new route at the end of the current list of routes.
   *
   * A route string is a string with 2 special constructions:
   * - :string: :string denotes a named paramater (available later as $request->getParameter('string'))
   * - *: * match an indefinite number of parameters in a route
   *
   * Here is a very common rule in a symfony project:
   *
   * <code>
   * $r->connect('default', '/:module/:action/*');
   * </code>
   *
   * @param  string The route name
   * @param  string The route string
   * @param  array  The default parameter values
   * @param  array  The regexps parameters must match
   *
   * @return array  current routes
   */
  
public function connect($name$route$defaults = array(), $requirements = array())
  {
    
// route already exists?
    
if (isset($this->routes[$name]))
    {
      throw new 
sfConfigurationException(sprintf('This named route already exists ("%s").'$name));
    }

    
$suffix $this->defaultSuffix;
    
$route  trim($route);

    
// fix defaults
    
foreach ($defaults as $key => $value)
    {
      if (
ctype_digit($key))
      {
        
$defaults[$value] = true;
      }
      else
      {
        
$defaults[$key] = urldecode($value);
      }
    }
    
$givenDefaults $defaults;
    
$defaults $this->fixDefaults($defaults);

    
// fix requirements regexs
    
foreach ($requirements as $key => $regex)
    {
      if (
'^' == $regex[0])
      {
        
$regex substr($regex1);
      }
      if (
'$' == substr($regex, -1))
      {
        
$regex substr($regex0, -1);
      }

      
$requirements[$key] = $regex;
    }

    
// a route can start by a slash. remove it for parsing.
    
if (!empty($route) && '/' == $route[0])
    {
      
$route substr($route1);
    }

    if (
$route == '')
    {
      
$this->routes[$name] = array('/''/^\/*$/', array(), $defaults$requirements);
    }
    else
    {
      
// ignore the default suffix if one is already provided in the route
      
if ('/' == $route[strlen($route) - 1])
      {
        
// route ends by / (directory)
        
$suffix '';
      }
      else if (
'.' == $route[strlen($route) - 1])
      {
        
// route ends by . (no suffix)
        
$suffix '';
        
$route substr($route0strlen($route) -1);
      }
      else if (
preg_match('#\.(?:'.$this->options['variable_prefix_regex'].$this->options['variable_regex'].'|'.$this->options['variable_content_regex'].')$#i'$route))
      {
        
// specific suffix for this route
        // a . with a variable after or some cars without any separators
        
$suffix '';
      }

      
// parse the route
      
$segments = array();
      
$firstOptional 0;
      
$buffer $route;
      
$afterASeparator true;
      
$currentSeparator '';
      
$variables = array();

      
// a route is an array of (separator + variable) or (separator + text) segments
      
while (strlen($buffer))
      {
        if (
$afterASeparator && preg_match('#^'.$this->options['variable_prefix_regex'].'('.$this->options['variable_regex'].')#'$buffer$match))
        {
          
// a variable (like :foo)
          
$variable $match[1];

          
//Removed requirement rexex for populated routing
          
$segments[] = $currentSeparator.'(?P<'.$variable.'>'.$this->options['variable_content_regex'].')';
          
$currentSeparator '';

          
// for 1.0 BC, we don't take into account the default module and action variable
          // for 1.2, remove the $givenDefaults var and move the $firstOptional setting to
          // the condition below
          
if (!isset($givenDefaults[$variable]))
          {
            
$firstOptional count($segments);
          }

          if (!isset(
$defaults[$variable]))
          {
            
$defaults[$variable] = null;
          }

          
$buffer substr($bufferstrlen($match[0]));
          
$variables[$variable] = $match[0];
          
$afterASeparator false;
        }
        else if (
$afterASeparator)
        {
          
// a static text
          
if (!preg_match('#^(.+?)(?:'.$this->options['segment_separators_regex'].'|$)#'$buffer$match))
          {
            throw new 
InvalidArgumentException(sprintf('Unable to parse "%s" route near "%s".'$route$buffer));
          }

          if (
'*' == $match[1])
          {
            
$segments[] = '(?:'.$currentSeparator.'(?P<_star>.*))?';
          }
          else
          {
            
$segments[] = $currentSeparator.preg_quote($match[1], '#');
            
$firstOptional count($segments);
          }
          
$currentSeparator '';

          
$buffer substr($bufferstrlen($match[1]));
          
$afterASeparator false;
        }
        else if (
preg_match('#^'.$this->options['segment_separators_regex'].'#'$buffer$match))
        {
          
// a separator (like / or .)
          
$currentSeparator preg_quote($match[0], '#');

          
$buffer substr($bufferstrlen($match[0]));
          
$afterASeparator true;
        }
        else
        {
          
// parsing problem
          
throw new InvalidArgumentException(sprintf('Unable to parse "%s" route near "%s".'$route$buffer));
        }
      }

      
// all segments after the last static segment are optional
      // be careful, the n-1 is optional only if n is empty
      
for ($i $firstOptional$max count($segments); $i $max$i++)
      {
        
$segments[$i] = str_repeat(' '$i $firstOptional).'(?:'.$segments[$i];
        
$segments[] = str_repeat(' '$max $i 1).')?';
      }

      
$regex "#^/\n".implode("\n"$segments)."\n".$currentSeparator.preg_quote($suffix'#')."$#x";
      
$this->routes[$name] = array('/'.$route.$suffix$regex$variables$defaults$requirements);
    }

    if (
$this->options['logging'])
    {
      
$this->dispatcher->notify(new sfEvent($this'application.log', array(sprintf('Connect "/%s"%s'$route$suffix ' ("'.$suffix.'" suffix)' ''))));
    }

    return 
$this->routes;
  }

  
/**
   * @see sfRouting
   */
  
public function generate($name$params = array(), $querydiv '/'$divider '/'$equals '/')
  {
    
$params $this->fixDefaults($params);

    if (!
is_null($this->cache))
    {
      
$cacheKey 'generate_'.$name.serialize(array_merge($this->defaultParameters$params));
      if (isset(
$this->cacheData[$cacheKey]))
      {
        return 
$this->cacheData[$cacheKey];
      }
    }

    
// named route?
    
if ($name)
    {
      if (!isset(
$this->routes[$name]))
      {
        throw new 
sfConfigurationException(sprintf('The route "%s" does not exist.'$name));
      }

      list(
$url$regex$variables$defaults$requirements) = $this->routes[$name];
      
$defaults $this->mergeArrays($defaults$this->defaultParameters);
      
$tparams $this->mergeArrays($defaults$params);

      
// all params must be given
      
if ($diff array_diff_key($variablesarray_filter($tparamscreate_function('$v''return !is_null($v);'))))
      {
        throw new 
InvalidArgumentException(sprintf('The "%s" route has some missing mandatory parameters (%s).'$nameimplode(', '$diff)));
      }
    }
    else
    {
      
// find a matching route
      
$found false;
      foreach (
$this->routes as $name => $route)
      {
        list(
$url$regex$variables$defaults$requirements) = $route;
        
$defaults $this->mergeArrays($defaults$this->defaultParameters);
        
$tparams $this->mergeArrays($defaults$params);

        
// all $variables must be defined in the $tparams array
        
if (array_diff_key($variablesarray_filter($tparams)))
        {
          continue;
        }

        
// check requirements
        
foreach ($requirements as $reqParam => $reqRegexp)
        {
          
/* removed for Populated Routing
          if (!is_null($tparams[$reqParam]) && !preg_match('#'.$reqRegexp.'#', $tparams[$reqParam]))
          {
            continue 2;
          }*/
        
}

        
// all $params must be in $variables or $defaults if there is no * in route
        
if (false === strpos($regex'_star') && array_diff_key(array_filter($params), $variables$defaults))
        {
          continue;
        }

        
// check that $params does not override a default value that is not a variable
        
foreach (array_filter($defaults) as $key => $value)
        {
          if (!isset(
$variables[$key]) && $tparams[$key] != $value)
          {
            continue 
2;
          }
        }

        
// found
        
$found true;
        break;
      }

      if (!
$found)
      {
        throw new 
sfConfigurationException(sprintf('Unable to find a matching routing rule to generate url for params "%s".'var_export($paramstrue)));
      }
    }

    
// replace variables
    
$realUrl $url;
    foreach (
$variables as $variable => $value)
    {
      
$realUrl str_replace($valueurlencode($tparams[$variable]), $realUrl);
    }

    
// add extra params if the route contains *
    
if (false !== strpos($regex'_star'))
    {
      
$tmp = array();
      foreach (
array_diff_key($tparams$variables$defaults) as $key => $value)
      {
        if (
is_array($value))
        {
          foreach (
$value as $v)
          {
            
$tmp[] = $key.$equals.urlencode($v);
          }
        }
        else
        {
          
$tmp[] = urlencode($key).$equals.urlencode($value);
        }
      }
      
$tmp implode($divider$tmp);
      if (
$tmp)
      {
        
$tmp $querydiv.$tmp;
      }

      
$realUrl preg_replace('#'.$this->options['segment_separators_regex'].'\*('.$this->options['segment_separators_regex'].'|$)#'"$tmp$1"$realUrl);
    }

    if (!
is_null($this->cache))
    {
      
$this->cacheChanged true;
      
$this->cacheData[$cacheKey] = $realUrl;
    }

    return 
$realUrl;
  }

  
/**
   * @see sfRouting
   */
  
public function parse($url)
  {
    
// an URL should start with a '/', mod_rewrite doesn't respect that, but no-mod_rewrite version does.
    
if ('/' != $url[0])
    {
      
$url '/'.$url;
    }

    
// we remove the query string
    
if (false !== $pos strpos($url'?'))
    {
      
$url substr($url0$pos);
    }

    
// remove multiple /
    
$url preg_replace('#/+#''/'$url);

    if (!
is_null($this->cache))
    {
      
$cacheKey 'parse_'.$url;
      if (isset(
$this->cacheData[$cacheKey]))
      {
        return 
$this->cacheData[$cacheKey];
      }
    }

    
$found false;
    foreach (
$this->routes as $routeName => $route)
    {
      list(
$route$regex$variables$defaults$requirements) = $route;
      if (!
preg_match($regex$url$r))
      {
        continue;
      }
      
      
$defaults array_merge($defaults$this->defaultParameters);
      
$found    true;
      
$out      = array();

      
// *
      
if (isset($r['_star']))
      {
        
$out $this->parseStarParameter($r['_star']);
        unset(
$r['_star']);
      }

      
// defaults
      
$out $this->mergeArrays($out$defaults);

      
// variables
      
foreach ($r as $key => $value)
      {
        if (!
is_int($key))
        {
          
$out[$key] = urldecode($value);
          if (
$requirements[$key])
          {
            
$callable $requirements[$key];
            
$param $out[$key];
            
$out[$key] = null;
            
//PopulatedRouting logic
            
if (is_callable($callable))
            {
              
//is a static function
              
$out[$key] = call_user_func($callable,$param);
            }
            else
            {
              
$callable $callable.'Peer::retrieveByPk';
              if (
is_callable($callable))
              {
                
//is a known DB object
                
$out[$key] = call_user_func($callable,$param);
              }
              else
              {
                throw new 
sfError404Exception(sprintf('Could not call "%s" with parameter "%s".'$requirements[$key], $param));
              }
            }
            if (
is_null($out[$key]))
            {
              throw new 
sfError404Exception(sprintf('Populating "%s" with parameter "%s" did not return any results.'$key$param));
            }
          }
        }
      }

      
// store the route name
      
$this->currentRouteName $routeName;
      
$this->currentInternalUri = array();

      if (
$this->options['logging'])
      {
        
$this->dispatcher->notify(new sfEvent($this'application.log', array(sprintf('Match route [%s] for "%s"'$routeName$route))));
      }

      break;
    }

    
// no route found
    
if (!$found)
    {
      throw new 
sfError404Exception(sprintf('No matching route found for "%s"'$url));
    }

    
$this->currentRouteParameters $this->fixDefaults($out);

    if (!
is_null($this->cache))
    {
      
$this->cacheChanged true;
      
$this->cacheData[$cacheKey] = $this->currentRouteParameters;
    }

    return 
$this->currentRouteParameters;
  }

}