This repository has been archived on 2023-08-20. You can view files and clone it, but cannot push or open issues or pull requests.
symfony/src/Symfony/Component/Routing/Generator/UrlGenerator.php

223 lines
8.3 KiB
PHP
Raw Normal View History

2010-02-17 13:53:31 +00:00
<?php
/*
* This file is part of the Symfony package.
2010-02-17 13:53:31 +00:00
*
* (c) Fabien Potencier <fabien@symfony.com>
2010-02-17 13:53:31 +00:00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
2010-02-17 13:53:31 +00:00
*/
namespace Symfony\Component\Routing\Generator;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Exception\InvalidParameterException;
2011-05-17 15:51:56 +01:00
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
2010-02-17 13:53:31 +00:00
/**
* UrlGenerator generates a URL based on a set of routes.
2010-02-17 13:53:31 +00:00
*
* @author Fabien Potencier <fabien@symfony.com>
2011-06-14 14:35:32 +01:00
*
* @api
2010-02-17 13:53:31 +00:00
*/
class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInterface
2010-02-17 13:53:31 +00:00
{
protected $context;
protected $strictRequirements = true;
protected $logger;
/**
* This array defines the characters (besides alphanumeric ones) that will not be percent-encoded in the path segment of the generated URL.
*
* PHP's rawurlencode() encodes all chars except "a-zA-Z0-9-._~" according to RFC 3986. But we want to allow some chars
* to be used in their literal form (reasons below). Other chars inside the path must of course be encoded, e.g.
* "?" and "#" (would be interpreted wrongly as query and fragment identifier),
* "'" and """ (are used as delimiters in HTML).
*/
protected $decodedChars = array(
// the slash can be used to designate a hierarchical structure and we want allow using it with this meaning
// some webservers don't allow the slash in encoded form in the path for security reasons anyway
// see http://stackoverflow.com/questions/4069002/http-400-if-2f-part-of-get-url-in-jboss
'%2F' => '/',
// the following chars are general delimiters in the URI specification but have only special meaning in the authority component
// so they can safely be used in the path in unencoded form
'%40' => '@',
'%3A' => ':',
// these chars are only sub-delimiters that have no predefined meaning and can therefore be used literally
// so URI producing applications can use these chars to delimit subcomponents in a path segment without being encoded for better readability
'%3B' => ';',
'%2C' => ',',
'%3D' => '=',
'%2B' => '+',
'%21' => '!',
'%2A' => '*',
'%7C' => '|',
2011-07-07 08:38:15 +01:00
);
2011-03-23 18:24:18 +00:00
protected $routes;
/**
* Constructor.
*
2011-04-23 16:05:44 +01:00
* @param RouteCollection $routes A RouteCollection instance
* @param RequestContext $context The context
* @param LoggerInterface $logger A logger instance
2011-06-14 14:35:32 +01:00
*
* @api
*/
public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $logger = null)
2010-02-17 13:53:31 +00:00
{
$this->routes = $routes;
$this->context = $context;
$this->logger = $logger;
2010-02-17 13:53:31 +00:00
}
/**
* {@inheritdoc}
*/
public function setContext(RequestContext $context)
{
$this->context = $context;
}
2011-04-21 20:20:27 +01:00
/**
* {@inheritdoc}
2011-04-21 20:20:27 +01:00
*/
public function getContext()
{
return $this->context;
}
/**
* {@inheritdoc}
*/
public function setStrictRequirements($enabled)
{
$this->strictRequirements = (Boolean) $enabled;
}
/**
* {@inheritdoc}
*/
public function isStrictRequirements()
{
return $this->strictRequirements;
}
/**
* {@inheritDoc}
*/
public function generate($name, $parameters = array(), $absolute = false)
2010-02-17 13:53:31 +00:00
{
made some method name changes to have a better coherence throughout the framework When an object has a "main" many relation with related "things" (objects, parameters, ...), the method names are normalized: * get() * set() * all() * replace() * remove() * clear() * isEmpty() * add() * register() * count() * keys() The classes below follow this method naming convention: * BrowserKit\CookieJar -> Cookie * BrowserKit\History -> Request * Console\Application -> Command * Console\Application\Helper\HelperSet -> HelperInterface * DependencyInjection\Container -> services * DependencyInjection\ContainerBuilder -> services * DependencyInjection\ParameterBag\ParameterBag -> parameters * DependencyInjection\ParameterBag\FrozenParameterBag -> parameters * DomCrawler\Form -> FormField * EventDispatcher\Event -> parameters * Form\FieldGroup -> Field * HttpFoundation\HeaderBag -> headers * HttpFoundation\ParameterBag -> parameters * HttpFoundation\Session -> attributes * HttpKernel\Profiler\Profiler -> DataCollectorInterface * Routing\RouteCollection -> Route * Security\Authentication\AuthenticationProviderManager -> AuthenticationProviderInterface * Templating\Engine -> HelperInterface * Translation\MessageCatalogue -> messages The usage of these methods are only allowed when it is clear that there is a main relation: * a CookieJar has many Cookies; * a Container has many services and many parameters (as services is the main relation, we use the naming convention for this relation); * a Console Input has many arguments and many options. There is no "main" relation, and so the naming convention does not apply. For many relations where the convention does not apply, the following methods must be used instead (where XXX is the name of the related thing): * get() -> getXXX() * set() -> setXXX() * all() -> getXXXs() * replace() -> setXXXs() * remove() -> removeXXX() * clear() -> clearXXX() * isEmpty() -> isEmptyXXX() * add() -> addXXX() * register() -> registerXXX() * count() -> countXXX() * keys()
2010-11-23 08:42:19 +00:00
if (null === $route = $this->routes->get($name)) {
2011-05-17 15:51:56 +01:00
throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name));
}
2010-02-17 13:53:31 +00:00
// the Route has a cache of its own and is not recompiled as long as it does not get modified
$compiledRoute = $route->compile();
2010-02-17 13:53:31 +00:00
return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $absolute);
2010-02-17 13:53:31 +00:00
}
/**
* @throws MissingMandatoryParametersException When route has some missing mandatory parameters
* @throws InvalidParameterException When a parameter value is not correct
*/
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute)
2010-02-17 13:53:31 +00:00
{
2011-04-25 11:03:41 +01:00
$variables = array_flip($variables);
$originParameters = $parameters;
$parameters = array_replace($this->context->getParameters(), $parameters);
$tparams = array_replace($defaults, $parameters);
// all params must be given
if ($diff = array_diff_key($variables, $tparams)) {
2011-05-26 11:54:21 +01:00
throw new MissingMandatoryParametersException(sprintf('The "%s" route has some missing mandatory parameters ("%s").', $name, implode('", "', array_keys($diff))));
2010-02-17 13:53:31 +00:00
}
$url = '';
$optional = true;
foreach ($tokens as $token) {
if ('variable' === $token[0]) {
if (false === $optional || !array_key_exists($token[3], $defaults) || (isset($parameters[$token[3]]) && (string) $parameters[$token[3]] != (string) $defaults[$token[3]])) {
if (!$isEmpty = in_array($tparams[$token[3]], array(null, '', false), true)) {
// check requirement
if ($tparams[$token[3]] && !preg_match('#^'.$token[2].'$#', $tparams[$token[3]])) {
$message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given).', $token[3], $name, $token[2], $tparams[$token[3]]);
if ($this->strictRequirements) {
throw new InvalidParameterException($message);
}
if ($this->logger) {
$this->logger->err($message);
}
return null;
}
}
if (!$isEmpty || !$optional) {
$url = $token[1].$tparams[$token[3]].$url;
}
$optional = false;
}
} elseif ('text' === $token[0]) {
2011-04-25 11:03:41 +01:00
$url = $token[1].$url;
$optional = false;
}
2010-02-17 13:53:31 +00:00
}
if ('' === $url) {
$url = '/';
}
2010-02-17 13:53:31 +00:00
// do not encode the contexts base url as it is already encoded (see Symfony\Component\HttpFoundation\Request)
$url = $this->context->getBaseUrl().strtr(rawurlencode($url), $this->decodedChars);
// the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3
// so we need to encode them as they are not used for this purpose here
// otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route
$url = strtr($url, array('/../' => '/%2E%2E/', '/./' => '/%2E/'));
if ('/..' === substr($url, -3)) {
$url = substr($url, 0, -2) . '%2E%2E';
} elseif ('/.' === substr($url, -2)) {
$url = substr($url, 0, -1) . '%2E';
}
// add a query string if needed
$extra = array_diff_key($originParameters, $variables, $defaults);
2012-04-23 08:55:54 +01:00
if ($extra && $query = http_build_query($extra, '', '&')) {
$url .= '?'.$query;
}
2010-02-17 13:53:31 +00:00
if ($this->context->getHost()) {
$scheme = $this->context->getScheme();
if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme != $req) {
$absolute = true;
$scheme = $req;
}
if ($absolute) {
$port = '';
if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
$port = ':'.$this->context->getHttpPort();
} elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) {
$port = ':'.$this->context->getHttpsPort();
}
$url = $scheme.'://'.$this->context->getHost().$port.$url;
}
}
2010-02-17 13:53:31 +00:00
return $url;
}
2010-02-17 13:53:31 +00:00
}