Update to laravel 7

This commit is contained in:
KodeStar
2022-03-10 11:54:29 +00:00
parent 61a5a1a8b0
commit f9a19fce91
7170 changed files with 274189 additions and 283773 deletions

View File

@@ -1,3 +0,0 @@
vendor/
composer.lock
phpunit.xml

96
vendor/symfony/routing/Alias.php vendored Normal file
View File

@@ -0,0 +1,96 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing;
use Symfony\Component\Routing\Exception\InvalidArgumentException;
class Alias
{
private $id;
private $deprecation = [];
public function __construct(string $id)
{
$this->id = $id;
}
/**
* @return static
*/
public function withId(string $id): self
{
$new = clone $this;
$new->id = $id;
return $new;
}
/**
* Returns the target name of this alias.
*
* @return string The target name
*/
public function getId(): string
{
return $this->id;
}
/**
* Whether this alias is deprecated, that means it should not be referenced anymore.
*
* @param string $package The name of the composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The deprecation message to use
*
* @return $this
*
* @throws InvalidArgumentException when the message template is invalid
*/
public function setDeprecated(string $package, string $version, string $message): self
{
if ('' !== $message) {
if (preg_match('#[\r\n]|\*/#', $message)) {
throw new InvalidArgumentException('Invalid characters found in deprecation template.');
}
if (!str_contains($message, '%alias_id%')) {
throw new InvalidArgumentException('The deprecation template must contain the "%alias_id%" placeholder.');
}
}
$this->deprecation = [
'package' => $package,
'version' => $version,
'message' => $message ?: 'The "%alias_id%" route alias is deprecated. You should stop using it, as it will be removed in the future.',
];
return $this;
}
public function isDeprecated(): bool
{
return (bool) $this->deprecation;
}
/**
* @param string $name Route name relying on this alias
*/
public function getDeprecation(string $name): array
{
return [
'package' => $this->deprecation['package'],
'version' => $this->deprecation['version'],
'message' => str_replace('%alias_id%', $name, $this->deprecation['message']),
];
}
}

View File

@@ -15,10 +15,13 @@ namespace Symfony\Component\Routing\Annotation;
* Annotation class for @Route().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"CLASS", "METHOD"})
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Alexander M. Turek <me@derrabus.de>
*/
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
class Route
{
private $path;
@@ -31,19 +34,81 @@ class Route
private $methods = [];
private $schemes = [];
private $condition;
private $locale;
private $format;
private $utf8;
private $priority;
private $env;
/**
* @param array $data An array of key/value parameters
* @param array|string $data data array managed by the Doctrine Annotations library or the path
* @param array|string|null $path
* @param string[] $requirements
* @param string[]|string $methods
* @param string[]|string $schemes
*
* @throws \BadMethodCallException
*/
public function __construct(array $data)
{
public function __construct(
$data = [],
$path = null,
string $name = null,
array $requirements = [],
array $options = [],
array $defaults = [],
string $host = null,
$methods = [],
$schemes = [],
string $condition = null,
int $priority = null,
string $locale = null,
string $format = null,
bool $utf8 = null,
bool $stateless = null,
string $env = null
) {
if (\is_string($data)) {
$data = ['path' => $data];
} elseif (!\is_array($data)) {
throw new \TypeError(sprintf('"%s": Argument $data is expected to be a string or array, got "%s".', __METHOD__, get_debug_type($data)));
} elseif ([] !== $data) {
$deprecation = false;
foreach ($data as $key => $val) {
if (\in_array($key, ['path', 'name', 'requirements', 'options', 'defaults', 'host', 'methods', 'schemes', 'condition', 'priority', 'locale', 'format', 'utf8', 'stateless', 'env', 'value'])) {
$deprecation = true;
}
}
if ($deprecation) {
trigger_deprecation('symfony/routing', '5.3', 'Passing an array as first argument to "%s" is deprecated. Use named arguments instead.', __METHOD__);
} else {
$localizedPaths = $data;
$data = ['path' => $localizedPaths];
}
}
if (null !== $path && !\is_string($path) && !\is_array($path)) {
throw new \TypeError(sprintf('"%s": Argument $path is expected to be a string, array or null, got "%s".', __METHOD__, get_debug_type($path)));
}
$data['path'] = $data['path'] ?? $path;
$data['name'] = $data['name'] ?? $name;
$data['requirements'] = $data['requirements'] ?? $requirements;
$data['options'] = $data['options'] ?? $options;
$data['defaults'] = $data['defaults'] ?? $defaults;
$data['host'] = $data['host'] ?? $host;
$data['methods'] = $data['methods'] ?? $methods;
$data['schemes'] = $data['schemes'] ?? $schemes;
$data['condition'] = $data['condition'] ?? $condition;
$data['priority'] = $data['priority'] ?? $priority;
$data['locale'] = $data['locale'] ?? $locale;
$data['format'] = $data['format'] ?? $format;
$data['utf8'] = $data['utf8'] ?? $utf8;
$data['stateless'] = $data['stateless'] ?? $stateless;
$data['env'] = $data['env'] ?? $env;
$data = array_filter($data, static function ($value): bool {
return null !== $value;
});
if (isset($data['localized_paths'])) {
throw new \BadMethodCallException(sprintf('Unknown property "localized_paths" on annotation "%s".', \get_class($this)));
throw new \BadMethodCallException(sprintf('Unknown property "localized_paths" on annotation "%s".', static::class));
}
if (isset($data['value'])) {
@@ -67,20 +132,25 @@ class Route
}
if (isset($data['utf8'])) {
$data['options']['utf8'] = filter_var($data['utf8'], FILTER_VALIDATE_BOOLEAN) ?: false;
$data['options']['utf8'] = filter_var($data['utf8'], \FILTER_VALIDATE_BOOLEAN) ?: false;
unset($data['utf8']);
}
if (isset($data['stateless'])) {
$data['defaults']['_stateless'] = filter_var($data['stateless'], \FILTER_VALIDATE_BOOLEAN) ?: false;
unset($data['stateless']);
}
foreach ($data as $key => $value) {
$method = 'set'.str_replace('_', '', $key);
if (!method_exists($this, $method)) {
throw new \BadMethodCallException(sprintf('Unknown property "%s" on annotation "%s".', $key, \get_class($this)));
throw new \BadMethodCallException(sprintf('Unknown property "%s" on annotation "%s".', $key, static::class));
}
$this->$method($value);
}
}
public function setPath($path)
public function setPath(string $path)
{
$this->path = $path;
}
@@ -100,7 +170,7 @@ class Route
return $this->localizedPaths;
}
public function setHost($pattern)
public function setHost(string $pattern)
{
$this->host = $pattern;
}
@@ -110,7 +180,7 @@ class Route
return $this->host;
}
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
@@ -120,7 +190,7 @@ class Route
return $this->name;
}
public function setRequirements($requirements)
public function setRequirements(array $requirements)
{
$this->requirements = $requirements;
}
@@ -130,7 +200,7 @@ class Route
return $this->requirements;
}
public function setOptions($options)
public function setOptions(array $options)
{
$this->options = $options;
}
@@ -140,7 +210,7 @@ class Route
return $this->options;
}
public function setDefaults($defaults)
public function setDefaults(array $defaults)
{
$this->defaults = $defaults;
}
@@ -170,7 +240,7 @@ class Route
return $this->methods;
}
public function setCondition($condition)
public function setCondition(?string $condition)
{
$this->condition = $condition;
}
@@ -179,4 +249,24 @@ class Route
{
return $this->condition;
}
public function setPriority(int $priority): void
{
$this->priority = $priority;
}
public function getPriority(): ?int
{
return $this->priority;
}
public function setEnv(?string $env): void
{
$this->env = $env;
}
public function getEnv(): ?string
{
return $this->env;
}
}

View File

@@ -1,6 +1,50 @@
CHANGELOG
=========
5.3
---
* Already encoded slashes are not decoded nor double-encoded anymore when generating URLs
* Add support for per-env configuration in XML and Yaml loaders
* Deprecate creating instances of the `Route` annotation class by passing an array of parameters
* Add `RoutingConfigurator::env()` to get the current environment
5.2.0
-----
* Added support for inline definition of requirements and defaults for host
* Added support for `\A` and `\z` as regex start and end for route requirement
* Added support for `#[Route]` attributes
5.1.0
-----
* added the protected method `PhpFileLoader::callConfigurator()` as extension point to ease custom routing configuration
* deprecated `RouteCollectionBuilder` in favor of `RoutingConfigurator`.
* added "priority" option to annotated routes
* added argument `$priority` to `RouteCollection::add()`
* deprecated the `RouteCompiler::REGEX_DELIMITER` constant
* added `ExpressionLanguageProvider` to expose extra functions to route conditions
* added support for a `stateless` keyword for configuring route stateless in PHP, YAML and XML configurations.
* added the "hosts" option to be able to configure the host per locale.
* added `RequestContext::fromUri()` to ease building the default context
5.0.0
-----
* removed `PhpGeneratorDumper` and `PhpMatcherDumper`
* removed `generator_base_class`, `generator_cache_class`, `matcher_base_class` and `matcher_cache_class` router options
* `Serializable` implementing methods for `Route` and `CompiledRoute` are final
* removed referencing service route loaders with a single colon
* Removed `ServiceRouterLoader` and `ObjectRouteLoader`.
4.4.0
-----
* Deprecated `ServiceRouterLoader` in favor of `ContainerLoader`.
* Deprecated `ObjectRouteLoader` in favor of `ObjectLoader`.
* Added a way to exclude patterns of resources from being imported by the `import()` method
4.3.0
-----
@@ -12,7 +56,7 @@ CHANGELOG
Instead of overwriting them, use `__serialize` and `__unserialize` as extension points which are forward compatible
with the new serialization methods in PHP 7.4.
* exposed `utf8` Route option, defaults "locale" and "format" in configuration loaders and configurators
* added support for invokable route loader services
* added support for invokable service route loaders
4.2.0
-----
@@ -37,15 +81,15 @@ CHANGELOG
3.3.0
-----
* [DEPRECATION] Class parameters have been deprecated and will be removed in 4.0.
* router.options.generator_class
* router.options.generator_base_class
* router.options.generator_dumper_class
* router.options.matcher_class
* router.options.matcher_base_class
* router.options.matcher_dumper_class
* router.options.matcher.cache_class
* router.options.generator.cache_class
* [DEPRECATION] Class parameters have been deprecated and will be removed in 4.0.
* router.options.generator_class
* router.options.generator_base_class
* router.options.generator_dumper_class
* router.options.matcher_class
* router.options.matcher_base_class
* router.options.matcher_dumper_class
* router.options.matcher.cache_class
* router.options.generator.cache_class
3.2.0
-----

View File

@@ -64,10 +64,9 @@ class CompiledRoute implements \Serializable
}
/**
* @internal since Symfony 4.3
* @final since Symfony 4.3
* @internal
*/
public function serialize()
final public function serialize(): string
{
return serialize($this->__serialize());
}
@@ -85,10 +84,9 @@ class CompiledRoute implements \Serializable
}
/**
* @internal since Symfony 4.3
* @final since Symfony 4.3
* @internal
*/
public function unserialize($serialized)
final public function unserialize($serialized)
{
$this->__unserialize(unserialize($serialized, ['allowed_classes' => false]));
}
@@ -96,7 +94,7 @@ class CompiledRoute implements \Serializable
/**
* Returns the static prefix.
*
* @return string The static prefix
* @return string
*/
public function getStaticPrefix()
{
@@ -106,7 +104,7 @@ class CompiledRoute implements \Serializable
/**
* Returns the regex.
*
* @return string The regex
* @return string
*/
public function getRegex()
{
@@ -116,7 +114,7 @@ class CompiledRoute implements \Serializable
/**
* Returns the host regex.
*
* @return string|null The host regex or null
* @return string|null
*/
public function getHostRegex()
{
@@ -126,7 +124,7 @@ class CompiledRoute implements \Serializable
/**
* Returns the tokens.
*
* @return array The tokens
* @return array
*/
public function getTokens()
{
@@ -136,7 +134,7 @@ class CompiledRoute implements \Serializable
/**
* Returns the host tokens.
*
* @return array The tokens
* @return array
*/
public function getHostTokens()
{
@@ -146,7 +144,7 @@ class CompiledRoute implements \Serializable
/**
* Returns the variables.
*
* @return array The variables
* @return array
*/
public function getVariables()
{
@@ -156,7 +154,7 @@ class CompiledRoute implements \Serializable
/**
* Returns the path variables.
*
* @return array The variables
* @return array
*/
public function getPathVariables()
{
@@ -166,7 +164,7 @@ class CompiledRoute implements \Serializable
/**
* Returns the host variables.
*
* @return array The variables
* @return array
*/
public function getHostVariables()
{

View File

@@ -30,6 +30,10 @@ class RoutingResolverPass implements CompilerPassInterface
public function __construct(string $resolverServiceId = 'routing.resolver', string $loaderTag = 'routing.loader')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/routing', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->resolverServiceId = $resolverServiceId;
$this->loaderTag = $loaderTag;
}

View File

@@ -9,11 +9,8 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses;
namespace Symfony\Component\Routing\Exception;
class BazClass
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
public function __invoke()
{
}
}

View File

@@ -22,8 +22,17 @@ class MethodNotAllowedException extends \RuntimeException implements ExceptionIn
{
protected $allowedMethods = [];
public function __construct(array $allowedMethods, string $message = null, int $code = 0, \Exception $previous = null)
/**
* @param string[] $allowedMethods
*/
public function __construct(array $allowedMethods, ?string $message = '', int $code = 0, \Throwable $previous = null)
{
if (null === $message) {
trigger_deprecation('symfony/routing', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__);
$message = '';
}
$this->allowedMethods = array_map('strtoupper', $allowedMethods);
parent::__construct($message, $code, $previous);
@@ -32,7 +41,7 @@ class MethodNotAllowedException extends \RuntimeException implements ExceptionIn
/**
* Gets the allowed HTTP methods.
*
* @return array
* @return string[]
*/
public function getAllowedMethods()
{

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Exception;
class RouteCircularReferenceException extends RuntimeException
{
public function __construct(string $routeId, array $path)
{
parent::__construct(sprintf('Circular reference detected for route "%s", path: "%s".', $routeId, implode(' -> ', $path)));
}
}

View File

@@ -9,8 +9,8 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses;
namespace Symfony\Component\Routing\Exception;
class FooClass
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -31,7 +31,7 @@ class CompiledUrlGenerator extends UrlGenerator
$this->defaultLocale = $defaultLocale;
}
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH)
{
$locale = $parameters['_locale']
?? $this->context->getParameter('_locale')
@@ -40,7 +40,6 @@ class CompiledUrlGenerator extends UrlGenerator
if (null !== $locale) {
do {
if (($this->compiledRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) {
unset($parameters['_locale']);
$name .= '.'.$locale;
break;
}
@@ -51,7 +50,19 @@ class CompiledUrlGenerator extends UrlGenerator
throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name));
}
list($variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes) = $this->compiledRoutes[$name];
[$variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes, $deprecations] = $this->compiledRoutes[$name] + [6 => []];
foreach ($deprecations as $deprecation) {
trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
}
if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) {
if (!\in_array('_locale', $variables, true)) {
unset($parameters['_locale']);
} elseif (!isset($parameters['_locale'])) {
$parameters['_locale'] = $defaults['_locale'];
}
}
return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes);
}

View File

@@ -40,10 +40,8 @@ interface ConfigurableRequirementsInterface
/**
* Enables or disables the exception on incorrect parameters.
* Passing null will deactivate the requirements check completely.
*
* @param bool|null $enabled
*/
public function setStrictRequirements($enabled);
public function setStrictRequirements(?bool $enabled);
/**
* Returns whether to throw an exception on incorrect parameters.

View File

@@ -11,6 +11,8 @@
namespace Symfony\Component\Routing\Generator\Dumper;
use Symfony\Component\Routing\Exception\RouteCircularReferenceException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
/**
@@ -35,12 +37,57 @@ class CompiledUrlGeneratorDumper extends GeneratorDumper
$compiledRoute->getTokens(),
$compiledRoute->getHostTokens(),
$route->getSchemes(),
[],
];
}
return $compiledRoutes;
}
public function getCompiledAliases(): array
{
$routes = $this->getRoutes();
$compiledAliases = [];
foreach ($routes->getAliases() as $name => $alias) {
$deprecations = $alias->isDeprecated() ? [$alias->getDeprecation($name)] : [];
$currentId = $alias->getId();
$visited = [];
while (null !== $alias = $routes->getAlias($currentId) ?? null) {
if (false !== $searchKey = array_search($currentId, $visited)) {
$visited[] = $currentId;
throw new RouteCircularReferenceException($currentId, \array_slice($visited, $searchKey));
}
if ($alias->isDeprecated()) {
$deprecations[] = $deprecation = $alias->getDeprecation($currentId);
trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
}
$visited[] = $currentId;
$currentId = $alias->getId();
}
if (null === $target = $routes->get($currentId)) {
throw new RouteNotFoundException(sprintf('Target route "%s" for alias "%s" does not exist.', $currentId, $name));
}
$compiledTarget = $target->compile();
$compiledAliases[$name] = [
$compiledTarget->getVariables(),
$target->getDefaults(),
$target->getRequirements(),
$compiledTarget->getTokens(),
$compiledTarget->getHostTokens(),
$target->getSchemes(),
$deprecations,
];
}
return $compiledAliases;
}
/**
* {@inheritdoc}
*/
@@ -68,6 +115,10 @@ EOF;
$routes .= sprintf("\n '%s' => %s,", $name, CompiledUrlMatcherDumper::export($properties));
}
foreach ($this->getCompiledAliases() as $alias => $properties) {
$routes .= sprintf("\n '%s' => %s,", $alias, CompiledUrlMatcherDumper::export($properties));
}
return $routes;
}
}

View File

@@ -24,16 +24,14 @@ interface GeneratorDumperInterface
* Dumps a set of routes to a string representation of executable code
* that can then be used to generate a URL of such a route.
*
* @param array $options An array of options
*
* @return string Executable code
* @return string
*/
public function dump(array $options = []);
/**
* Gets the routes to dump.
*
* @return RouteCollection A RouteCollection instance
* @return RouteCollection
*/
public function getRoutes();
}

View File

@@ -1,140 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Generator\Dumper;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "CompiledUrlGeneratorDumper" instead.', PhpGeneratorDumper::class), E_USER_DEPRECATED);
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
/**
* PhpGeneratorDumper creates a PHP class able to generate URLs for a given set of routes.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Tobias Schultze <http://tobion.de>
*
* @deprecated since Symfony 4.3, use CompiledUrlGeneratorDumper instead.
*/
class PhpGeneratorDumper extends GeneratorDumper
{
/**
* Dumps a set of routes to a PHP class.
*
* Available options:
*
* * class: The class name
* * base_class: The base class name
*
* @param array $options An array of options
*
* @return string A PHP class representing the generator class
*/
public function dump(array $options = [])
{
$options = array_merge([
'class' => 'ProjectUrlGenerator',
'base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
], $options);
return <<<EOF
<?php
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Psr\Log\LoggerInterface;
/**
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class {$options['class']} extends {$options['base_class']}
{
private static \$declaredRoutes;
private \$defaultLocale;
public function __construct(RequestContext \$context, LoggerInterface \$logger = null, string \$defaultLocale = null)
{
\$this->context = \$context;
\$this->logger = \$logger;
\$this->defaultLocale = \$defaultLocale;
if (null === self::\$declaredRoutes) {
self::\$declaredRoutes = {$this->generateDeclaredRoutes()};
}
}
{$this->generateGenerateMethod()}
}
EOF;
}
/**
* Generates PHP code representing an array of defined routes
* together with the routes properties (e.g. requirements).
*
* @return string PHP code
*/
private function generateDeclaredRoutes()
{
$routes = "[\n";
foreach ($this->getRoutes()->all() as $name => $route) {
$compiledRoute = $route->compile();
$properties = [];
$properties[] = $compiledRoute->getVariables();
$properties[] = $route->getDefaults();
$properties[] = $route->getRequirements();
$properties[] = $compiledRoute->getTokens();
$properties[] = $compiledRoute->getHostTokens();
$properties[] = $route->getSchemes();
$routes .= sprintf(" '%s' => %s,\n", $name, CompiledUrlMatcherDumper::export($properties));
}
$routes .= ' ]';
return $routes;
}
/**
* Generates PHP code representing the `generate` method that implements the UrlGeneratorInterface.
*
* @return string PHP code
*/
private function generateGenerateMethod()
{
return <<<'EOF'
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
$locale = $parameters['_locale']
?? $this->context->getParameter('_locale')
?: $this->defaultLocale;
if (null !== $locale && null !== $name) {
do {
if ((self::$declaredRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) {
unset($parameters['_locale']);
$name .= '.'.$locale;
break;
}
} while (false !== $locale = strstr($locale, '_', true));
}
if (!isset(self::$declaredRoutes[$name])) {
throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name));
}
list($variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes) = self::$declaredRoutes[$name];
return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes);
}
EOF;
}
}

View File

@@ -66,6 +66,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
// 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' => '/',
'%252F' => '%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' => '@',
@@ -108,9 +109,9 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
/**
* {@inheritdoc}
*/
public function setStrictRequirements($enabled)
public function setStrictRequirements(?bool $enabled)
{
$this->strictRequirements = null === $enabled ? null : (bool) $enabled;
$this->strictRequirements = $enabled;
}
/**
@@ -124,7 +125,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
/**
* {@inheritdoc}
*/
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH)
{
$route = null;
$locale = $parameters['_locale']
@@ -134,7 +135,6 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
if (null !== $locale) {
do {
if (null !== ($route = $this->routes->get($name.'.'.$locale)) && $route->getDefault('_canonical_route') === $name) {
unset($parameters['_locale']);
break;
}
} while (false !== $locale = strstr($locale, '_', true));
@@ -147,15 +147,28 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
// the Route has a cache of its own and is not recompiled as long as it does not get modified
$compiledRoute = $route->compile();
return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens(), $route->getSchemes());
$defaults = $route->getDefaults();
$variables = $compiledRoute->getVariables();
if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) {
if (!\in_array('_locale', $variables, true)) {
unset($parameters['_locale']);
} elseif (!isset($parameters['_locale'])) {
$parameters['_locale'] = $defaults['_locale'];
}
}
return $this->doGenerate($variables, $defaults, $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens(), $route->getSchemes());
}
/**
* @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route
* @throws InvalidParameterException When a parameter value for a placeholder is not correct because
* it does not match the requirement
*
* @return string
*/
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = [])
protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parameters, string $name, int $referenceType, array $hostTokens, array $requiredSchemes = [])
{
$variables = array_flip($variables);
$mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters);
@@ -176,7 +189,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
if (!$optional || $important || !\array_key_exists($varName, $defaults) || (null !== $mergedParams[$varName] && (string) $mergedParams[$varName] !== (string) $defaults[$varName])) {
// check requirement (while ignoring look-around patterns)
if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|<!)((?:[^()\\\\]+|\\\\.|\((?1)\))*)\)/', '', $token[2]).'$#i'.(empty($token[4]) ? '' : 'u'), $mergedParams[$token[3]])) {
if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|<!)((?:[^()\\\\]+|\\\\.|\((?1)\))*)\)/', '', $token[2]).'$#i'.(empty($token[4]) ? '' : 'u'), $mergedParams[$token[3]] ?? '')) {
if ($this->strictRequirements) {
throw new InvalidParameterException(strtr($message, ['{parameter}' => $varName, '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$varName]]));
}
@@ -209,9 +222,9 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
// 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, ['/../' => '/%2E%2E/', '/./' => '/%2E/']);
if ('/..' === substr($url, -3)) {
if (str_ends_with($url, '/..')) {
$url = substr($url, 0, -2).'%2E%2E';
} elseif ('/.' === substr($url, -2)) {
} elseif (str_ends_with($url, '/.')) {
$url = substr($url, 0, -1).'%2E';
}
@@ -257,16 +270,18 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
}
}
if ((self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) && !empty($host)) {
$port = '';
if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
$port = ':'.$this->context->getHttpPort();
} elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) {
$port = ':'.$this->context->getHttpsPort();
}
if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
if ('' !== $host || ('' !== $scheme && 'http' !== $scheme && 'https' !== $scheme)) {
$port = '';
if ('http' === $scheme && 80 !== $this->context->getHttpPort()) {
$port = ':'.$this->context->getHttpPort();
} elseif ('https' === $scheme && 443 !== $this->context->getHttpsPort()) {
$port = ':'.$this->context->getHttpsPort();
}
$schemeAuthority = self::NETWORK_PATH === $referenceType ? '//' : "$scheme://";
$schemeAuthority .= $host.$port;
$schemeAuthority = self::NETWORK_PATH === $referenceType || '' === $scheme ? '//' : "$scheme://";
$schemeAuthority .= $host.$port;
}
}
if (self::RELATIVE_PATH === $referenceType) {
@@ -280,6 +295,17 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
return $a == $b ? 0 : 1;
});
array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) {
if (\is_object($v)) {
if ($vars = get_object_vars($v)) {
array_walk_recursive($vars, $caster);
$v = $vars;
} elseif (method_exists($v, '__toString')) {
$v = (string) $v;
}
}
});
// extract fragment
$fragment = $defaults['_fragment'] ?? '';
@@ -288,7 +314,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
unset($extra['_fragment']);
}
if ($extra && $query = http_build_query($extra, '', '&', PHP_QUERY_RFC3986)) {
if ($extra && $query = http_build_query($extra, '', '&', \PHP_QUERY_RFC3986)) {
$url .= '?'.strtr($query, self::QUERY_FRAGMENT_DECODED);
}
@@ -317,9 +343,9 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
* @param string $basePath The base path
* @param string $targetPath The target path
*
* @return string The relative target path
* @return string
*/
public static function getRelativePath($basePath, $targetPath)
public static function getRelativePath(string $basePath, string $targetPath)
{
if ($basePath === $targetPath) {
return '';

View File

@@ -34,25 +34,25 @@ interface UrlGeneratorInterface extends RequestContextAwareInterface
/**
* Generates an absolute URL, e.g. "http://example.com/dir/file".
*/
const ABSOLUTE_URL = 0;
public const ABSOLUTE_URL = 0;
/**
* Generates an absolute path, e.g. "/dir/file".
*/
const ABSOLUTE_PATH = 1;
public const ABSOLUTE_PATH = 1;
/**
* Generates a relative path based on the current request path, e.g. "../parent-file".
*
* @see UrlGenerator::getRelativePath()
*/
const RELATIVE_PATH = 2;
public const RELATIVE_PATH = 2;
/**
* Generates a network path, e.g. "//example.com/dir/file".
* Such reference reuses the current scheme but specifies the host.
*/
const NETWORK_PATH = 3;
public const NETWORK_PATH = 3;
/**
* Generates a URL or path for a specific route based on the given parameters.
@@ -71,16 +71,12 @@ interface UrlGeneratorInterface extends RequestContextAwareInterface
*
* The special parameter _fragment will be used as the document fragment suffixed to the final URL.
*
* @param string $name The name of the route
* @param mixed $parameters An array of parameters
* @param int $referenceType The type of reference to be generated (one of the constants)
*
* @return string The generated URL
* @return string
*
* @throws RouteNotFoundException If the named route doesn't exist
* @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route
* @throws InvalidParameterException When a parameter value for a placeholder is not correct because
* it does not match the requirement
*/
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH);
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH);
}

View File

@@ -1,4 +1,4 @@
Copyright (c) 2004-2019 Fabien Potencier
Copyright (c) 2004-2022 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -15,13 +15,14 @@ use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Loader\LoaderResolverInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\Annotation\Route as RouteAnnotation;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* AnnotationClassLoader loads routing information from a PHP class and its methods.
*
* You need to define an implementation for the getRouteDefaults() method. Most of the
* You need to define an implementation for the configureRoute() method. Most of the
* time, this method should define some PHP callable to be called for the route
* (a controller in MVC speak).
*
@@ -32,7 +33,6 @@ use Symfony\Component\Routing\RouteCollection;
* recognizes several parameters: requirements, options, defaults, schemes,
* methods, host, and name. The name parameter is mandatory.
* Here is an example of how you should be able to use it:
*
* /**
* * @Route("/Blog")
* * /
@@ -44,7 +44,6 @@ use Symfony\Component\Routing\RouteCollection;
* public function index()
* {
* }
*
* /**
* * @Route("/{id}", name="blog_post", requirements = {"id" = "\d+"})
* * /
@@ -52,34 +51,50 @@ use Symfony\Component\Routing\RouteCollection;
* {
* }
* }
*
* On PHP 8, the annotation class can be used as an attribute as well:
* #[Route('/Blog')]
* class Blog
* {
* #[Route('/', name: 'blog_index')]
* public function index()
* {
* }
* #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])]
* public function show()
* {
* }
* }
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Alexander M. Turek <me@derrabus.de>
*/
abstract class AnnotationClassLoader implements LoaderInterface
{
protected $reader;
protected $env;
/**
* @var string
*/
protected $routeAnnotationClass = 'Symfony\\Component\\Routing\\Annotation\\Route';
protected $routeAnnotationClass = RouteAnnotation::class;
/**
* @var int
*/
protected $defaultRouteIndex = 0;
public function __construct(Reader $reader)
public function __construct(Reader $reader = null, string $env = null)
{
$this->reader = $reader;
$this->env = $env;
}
/**
* Sets the annotation class to read route properties from.
*
* @param string $class A fully-qualified class name
*/
public function setRouteAnnotationClass($class)
public function setRouteAnnotationClass(string $class)
{
$this->routeAnnotationClass = $class;
}
@@ -87,14 +102,13 @@ abstract class AnnotationClassLoader implements LoaderInterface
/**
* Loads from annotations from a class.
*
* @param string $class A class name
* @param string|null $type The resource type
* @param string $class A class name
*
* @return RouteCollection A RouteCollection instance
* @return RouteCollection
*
* @throws \InvalidArgumentException When route can't be parsed
*/
public function load($class, $type = null)
public function load($class, string $type = null)
{
if (!class_exists($class)) {
throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
@@ -110,29 +124,36 @@ abstract class AnnotationClassLoader implements LoaderInterface
$collection = new RouteCollection();
$collection->addResource(new FileResource($class->getFileName()));
if ($globals['env'] && $this->env !== $globals['env']) {
return $collection;
}
foreach ($class->getMethods() as $method) {
$this->defaultRouteIndex = 0;
foreach ($this->reader->getMethodAnnotations($method) as $annot) {
if ($annot instanceof $this->routeAnnotationClass) {
$this->addRoute($collection, $annot, $globals, $class, $method);
}
foreach ($this->getAnnotations($method) as $annot) {
$this->addRoute($collection, $annot, $globals, $class, $method);
}
}
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
$globals = $this->resetGlobals();
foreach ($this->reader->getClassAnnotations($class) as $annot) {
if ($annot instanceof $this->routeAnnotationClass) {
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
}
foreach ($this->getAnnotations($class) as $annot) {
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
}
}
return $collection;
}
protected function addRoute(RouteCollection $collection, $annot, $globals, \ReflectionClass $class, \ReflectionMethod $method)
/**
* @param RouteAnnotation $annot or an object that exposes a similar interface
*/
protected function addRoute(RouteCollection $collection, object $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method)
{
if ($annot->getEnv() && $annot->getEnv() !== $this->env) {
return;
}
$name = $annot->getName();
if (null === $name) {
$name = $this->getDefaultRouteName($class, $method);
@@ -143,7 +164,7 @@ abstract class AnnotationClassLoader implements LoaderInterface
foreach ($requirements as $placeholder => $requirement) {
if (\is_int($placeholder)) {
@trigger_error(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s::%s()"?', $placeholder, $requirement, $name, $class->getName(), $method->getName()), E_USER_DEPRECATED);
throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s::%s()"?', $placeholder, $requirement, $name, $class->getName(), $method->getName()));
}
}
@@ -158,10 +179,8 @@ abstract class AnnotationClassLoader implements LoaderInterface
$host = $globals['host'];
}
$condition = $annot->getCondition();
if (null === $condition) {
$condition = $globals['condition'];
}
$condition = $annot->getCondition() ?? $globals['condition'];
$priority = $annot->getPriority() ?? $globals['priority'];
$path = $annot->getLocalizedPaths() ?: $annot->getPath();
$prefix = $globals['localized_paths'] ?: $globals['path'];
@@ -208,10 +227,11 @@ abstract class AnnotationClassLoader implements LoaderInterface
$this->configureRoute($route, $class, $method, $annot);
if (0 !== $locale) {
$route->setDefault('_locale', $locale);
$route->setRequirement('_locale', preg_quote($locale));
$route->setDefault('_canonical_route', $name);
$collection->add($name.'.'.$locale, $route);
$collection->add($name.'.'.$locale, $route, $priority);
} else {
$collection->add($name, $route);
$collection->add($name, $route, $priority);
}
}
}
@@ -219,7 +239,7 @@ abstract class AnnotationClassLoader implements LoaderInterface
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports($resource, string $type = null)
{
return \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || 'annotation' === $type);
}
@@ -241,9 +261,6 @@ abstract class AnnotationClassLoader implements LoaderInterface
/**
* Gets the default route name for a class method.
*
* @param \ReflectionClass $class
* @param \ReflectionMethod $method
*
* @return string
*/
protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method)
@@ -262,7 +279,15 @@ abstract class AnnotationClassLoader implements LoaderInterface
{
$globals = $this->resetGlobals();
if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) {
$annot = null;
if (\PHP_VERSION_ID >= 80000 && ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)) {
$annot = $attribute->newInstance();
}
if (!$annot && $this->reader) {
$annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass);
}
if ($annot) {
if (null !== $annot->getName()) {
$globals['name'] = $annot->getName();
}
@@ -301,9 +326,12 @@ abstract class AnnotationClassLoader implements LoaderInterface
$globals['condition'] = $annot->getCondition();
}
$globals['priority'] = $annot->getPriority() ?? 0;
$globals['env'] = $annot->getEnv();
foreach ($globals['requirements'] as $placeholder => $requirement) {
if (\is_int($placeholder)) {
@trigger_error(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" in "%s"?', $placeholder, $requirement, $class->getName()), E_USER_DEPRECATED);
throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" in "%s"?', $placeholder, $requirement, $class->getName()));
}
}
}
@@ -311,7 +339,7 @@ abstract class AnnotationClassLoader implements LoaderInterface
return $globals;
}
private function resetGlobals()
private function resetGlobals(): array
{
return [
'path' => null,
@@ -324,13 +352,43 @@ abstract class AnnotationClassLoader implements LoaderInterface
'host' => '',
'condition' => '',
'name' => '',
'priority' => 0,
'env' => null,
];
}
protected function createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition)
protected function createRoute(string $path, array $defaults, array $requirements, array $options, ?string $host, array $schemes, array $methods, ?string $condition)
{
return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
}
abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot);
abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot);
/**
* @param \ReflectionClass|\ReflectionMethod $reflection
*
* @return iterable<int, RouteAnnotation>
*/
private function getAnnotations(object $reflection): iterable
{
if (\PHP_VERSION_ID >= 80000) {
foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
yield $attribute->newInstance();
}
}
if (!$this->reader) {
return;
}
$anntotations = $reflection instanceof \ReflectionClass
? $this->reader->getClassAnnotations($reflection)
: $this->reader->getMethodAnnotations($reflection);
foreach ($anntotations as $annotation) {
if ($annotation instanceof $this->routeAnnotationClass) {
yield $annotation;
}
}
}
}

View File

@@ -28,11 +28,11 @@ class AnnotationDirectoryLoader extends AnnotationFileLoader
* @param string $path A directory path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
* @return RouteCollection
*
* @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed
*/
public function load($path, $type = null)
public function load($path, string $type = null)
{
if (!is_dir($dir = $this->locator->locate($path))) {
return parent::supports($path, $type) ? parent::load($path, $type) : new RouteCollection();
@@ -54,7 +54,7 @@ class AnnotationDirectoryLoader extends AnnotationFileLoader
});
foreach ($files as $file) {
if (!$file->isFile() || '.php' !== substr($file->getFilename(), -4)) {
if (!$file->isFile() || !str_ends_with($file->getFilename(), '.php')) {
continue;
}
@@ -74,7 +74,7 @@ class AnnotationDirectoryLoader extends AnnotationFileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports($resource, string $type = null)
{
if ('annotation' === $type) {
return true;

View File

@@ -26,9 +26,6 @@ class AnnotationFileLoader extends FileLoader
{
protected $loader;
/**
* @throws \RuntimeException
*/
public function __construct(FileLocatorInterface $locator, AnnotationClassLoader $loader)
{
if (!\function_exists('token_get_all')) {
@@ -46,11 +43,11 @@ class AnnotationFileLoader extends FileLoader
* @param string $file A PHP file path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
* @return RouteCollection|null
*
* @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed
*/
public function load($file, $type = null)
public function load($file, string $type = null)
{
$path = $this->locator->locate($file);
@@ -58,7 +55,7 @@ class AnnotationFileLoader extends FileLoader
if ($class = $this->findClass($path)) {
$refl = new \ReflectionClass($class);
if ($refl->isAbstract()) {
return;
return null;
}
$collection->addResource(new FileResource($path));
@@ -73,59 +70,63 @@ class AnnotationFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports($resource, string $type = null)
{
return \is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'annotation' === $type);
return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'annotation' === $type);
}
/**
* Returns the full class name for the first class in the file.
*
* @param string $file A PHP file path
*
* @return string|false Full class name if found, false otherwise
* @return string|false
*/
protected function findClass($file)
protected function findClass(string $file)
{
$class = false;
$namespace = false;
$tokens = token_get_all(file_get_contents($file));
if (1 === \count($tokens) && T_INLINE_HTML === $tokens[0][0]) {
if (1 === \count($tokens) && \T_INLINE_HTML === $tokens[0][0]) {
throw new \InvalidArgumentException(sprintf('The file "%s" does not contain PHP code. Did you forgot to add the "<?php" start tag at the beginning of the file?', $file));
}
$nsTokens = [\T_NS_SEPARATOR => true, \T_STRING => true];
if (\defined('T_NAME_QUALIFIED')) {
$nsTokens[\T_NAME_QUALIFIED] = true;
}
for ($i = 0; isset($tokens[$i]); ++$i) {
$token = $tokens[$i];
if (!isset($token[1])) {
continue;
}
if (true === $class && T_STRING === $token[0]) {
if (true === $class && \T_STRING === $token[0]) {
return $namespace.'\\'.$token[1];
}
if (true === $namespace && T_STRING === $token[0]) {
if (true === $namespace && isset($nsTokens[$token[0]])) {
$namespace = $token[1];
while (isset($tokens[++$i][1]) && \in_array($tokens[$i][0], [T_NS_SEPARATOR, T_STRING])) {
while (isset($tokens[++$i][1], $nsTokens[$tokens[$i][0]])) {
$namespace .= $tokens[$i][1];
}
$token = $tokens[$i];
}
if (T_CLASS === $token[0]) {
if (\T_CLASS === $token[0]) {
// Skip usage of ::class constant and anonymous classes
$skipClassToken = false;
for ($j = $i - 1; $j > 0; --$j) {
if (!isset($tokens[$j][1])) {
if ('(' === $tokens[$j] || ',' === $tokens[$j]) {
$skipClassToken = true;
}
break;
}
if (T_DOUBLE_COLON === $tokens[$j][0] || T_NEW === $tokens[$j][0]) {
if (\T_DOUBLE_COLON === $tokens[$j][0] || \T_NEW === $tokens[$j][0]) {
$skipClassToken = true;
break;
} elseif (!\in_array($tokens[$j][0], [T_WHITESPACE, T_DOC_COMMENT, T_COMMENT])) {
} elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) {
break;
}
}
@@ -135,7 +136,7 @@ class AnnotationFileLoader extends FileLoader
}
}
if (T_NAMESPACE === $token[0]) {
if (\T_NAMESPACE === $token[0]) {
$namespace = true;
}
}

View File

@@ -29,17 +29,17 @@ class ClosureLoader extends Loader
* @param \Closure $closure A Closure
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
* @return RouteCollection
*/
public function load($closure, $type = null)
public function load($closure, string $type = null)
{
return $closure();
return $closure($this->env);
}
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports($resource, string $type = null)
{
return $resource instanceof \Closure && (!$type || 'closure' === $type);
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Loader\Configurator;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\Routing\Alias;
class AliasConfigurator
{
private $alias;
public function __construct(Alias $alias)
{
$this->alias = $alias;
}
/**
* Whether this alias is deprecated, that means it should not be called anymore.
*
* @param string $package The name of the composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The deprecation message to use
*
* @return $this
*
* @throws InvalidArgumentException when the message template is invalid
*/
public function deprecate(string $package, string $version, string $message): self
{
$this->alias->setDeprecated($package, $version, $message);
return $this;
}
}

View File

@@ -20,11 +20,13 @@ use Symfony\Component\Routing\RouteCollection;
class CollectionConfigurator
{
use Traits\AddTrait;
use Traits\HostTrait;
use Traits\RouteTrait;
private $parent;
private $parentConfigurator;
private $parentPrefixes;
private $host;
public function __construct(RouteCollection $parent, string $name, self $parentConfigurator = null, array $parentPrefixes = null)
{
@@ -36,21 +38,35 @@ class CollectionConfigurator
$this->parentPrefixes = $parentPrefixes;
}
/**
* @return array
*/
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
if (null === $this->prefixes) {
$this->collection->addPrefix($this->route->getPath());
}
if (null !== $this->host) {
$this->addHost($this->collection, $this->host);
}
$this->parent->addCollection($this->collection);
}
/**
* Creates a sub-collection.
*
* @return self
*/
final public function collection($name = '')
final public function collection(string $name = ''): self
{
return new self($this->collection, $this->name.$name, $this, $this->prefixes);
}
@@ -62,7 +78,7 @@ class CollectionConfigurator
*
* @return $this
*/
final public function prefix($prefix)
final public function prefix($prefix): self
{
if (\is_array($prefix)) {
if (null === $this->parentPrefixes) {
@@ -88,7 +104,21 @@ class CollectionConfigurator
return $this;
}
private function createRoute($path): Route
/**
* Sets the host to use for all child routes.
*
* @param string|array $host the host, or the localized hosts
*
* @return $this
*/
final public function host($host): self
{
$this->host = $host;
return $this;
}
private function createRoute(string $path): Route
{
return (clone $this->route)->setPath($path);
}

View File

@@ -11,7 +11,6 @@
namespace Symfony\Component\Routing\Loader\Configurator;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
@@ -19,6 +18,8 @@ use Symfony\Component\Routing\RouteCollection;
*/
class ImportConfigurator
{
use Traits\HostTrait;
use Traits\PrefixTrait;
use Traits\RouteTrait;
private $parent;
@@ -29,6 +30,19 @@ class ImportConfigurator
$this->route = $route;
}
/**
* @return array
*/
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->parent->addCollection($this->route);
@@ -41,40 +55,9 @@ class ImportConfigurator
*
* @return $this
*/
final public function prefix($prefix, bool $trailingSlashOnRoot = true)
final public function prefix($prefix, bool $trailingSlashOnRoot = true): self
{
if (!\is_array($prefix)) {
$this->route->addPrefix($prefix);
if (!$trailingSlashOnRoot) {
$rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath();
foreach ($this->route->all() as $route) {
if ($route->getPath() === $rootPath) {
$route->setPath(rtrim($rootPath, '/'));
}
}
}
} else {
foreach ($prefix as $locale => $localePrefix) {
$prefix[$locale] = trim(trim($localePrefix), '/');
}
foreach ($this->route->all() as $name => $route) {
if (null === $locale = $route->getDefault('_locale')) {
$this->route->remove($name);
foreach ($prefix as $locale => $localePrefix) {
$localizedRoute = clone $route;
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setDefault('_canonical_route', $name);
$localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$this->route->add($name.'.'.$locale, $localizedRoute);
}
} elseif (!isset($prefix[$locale])) {
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
} else {
$route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$this->route->add($name, $route);
}
}
}
$this->addPrefix($this->route, $prefix, $trailingSlashOnRoot);
return $this;
}
@@ -84,10 +67,24 @@ class ImportConfigurator
*
* @return $this
*/
final public function namePrefix(string $namePrefix)
final public function namePrefix(string $namePrefix): self
{
$this->route->addNamePrefix($namePrefix);
return $this;
}
/**
* Sets the host to use for all child routes.
*
* @param string|array $host the host, or the localized hosts
*
* @return $this
*/
final public function host($host): self
{
$this->addHost($this->route, $host);
return $this;
}
}

View File

@@ -19,11 +19,12 @@ use Symfony\Component\Routing\RouteCollection;
class RouteConfigurator
{
use Traits\AddTrait;
use Traits\HostTrait;
use Traits\RouteTrait;
private $parentConfigurator;
protected $parentConfigurator;
public function __construct(RouteCollection $collection, $route, string $name = '', CollectionConfigurator $parentConfigurator = null, array $prefixes = null)
public function __construct(RouteCollection $collection, RouteCollection $route, string $name = '', CollectionConfigurator $parentConfigurator = null, array $prefixes = null)
{
$this->collection = $collection;
$this->route = $route;
@@ -31,4 +32,18 @@ class RouteConfigurator
$this->parentConfigurator = $parentConfigurator; // for GC control
$this->prefixes = $prefixes;
}
/**
* Sets the host to use for all child routes.
*
* @param string|array $host the host, or the localized hosts
*
* @return $this
*/
final public function host($host): self
{
$this->addHost($this->route, $host);
return $this;
}
}

View File

@@ -24,22 +24,25 @@ class RoutingConfigurator
private $loader;
private $path;
private $file;
private $env;
public function __construct(RouteCollection $collection, PhpFileLoader $loader, string $path, string $file)
public function __construct(RouteCollection $collection, PhpFileLoader $loader, string $path, string $file, string $env = null)
{
$this->collection = $collection;
$this->loader = $loader;
$this->path = $path;
$this->file = $file;
$this->env = $env;
}
/**
* @return ImportConfigurator
* @param string|string[]|null $exclude Glob patterns to exclude from the import
*/
final public function import($resource, $type = null, $ignoreErrors = false)
final public function import($resource, string $type = null, bool $ignoreErrors = false, $exclude = null): ImportConfigurator
{
$this->loader->setCurrentDir(\dirname($this->path));
$imported = $this->loader->import($resource, $type, $ignoreErrors, $this->file);
$imported = $this->loader->import($resource, $type, $ignoreErrors, $this->file, $exclude) ?: [];
if (!\is_array($imported)) {
return new ImportConfigurator($this->collection, $imported);
}
@@ -52,11 +55,27 @@ class RoutingConfigurator
return new ImportConfigurator($this->collection, $mergedCollection);
}
/**
* @return CollectionConfigurator
*/
final public function collection($name = '')
final public function collection(string $name = ''): CollectionConfigurator
{
return new CollectionConfigurator($this->collection, $name);
}
/**
* Get the current environment to be able to write conditional configuration.
*/
final public function env(): ?string
{
return $this->env;
}
/**
* @return static
*/
final public function withPath(string $path): self
{
$clone = clone $this;
$clone->path = $clone->file = $path;
return $clone;
}
}

View File

@@ -11,66 +11,41 @@
namespace Symfony\Component\Routing\Loader\Configurator\Traits;
use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator;
use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator;
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
trait AddTrait
{
use LocalizedRouteTrait;
/**
* @var RouteCollection
*/
private $collection;
private $name = '';
private $prefixes;
protected $collection;
protected $name = '';
protected $prefixes;
/**
* Adds a route.
*
* @param string|array $path the path, or the localized paths of the route
*/
final public function add(string $name, $path): RouteConfigurator
public function add(string $name, $path): RouteConfigurator
{
$paths = [];
$parentConfigurator = $this instanceof CollectionConfigurator ? $this : ($this instanceof RouteConfigurator ? $this->parentConfigurator : null);
$route = $this->createLocalizedRoute($this->collection, $name, $path, $this->name, $this->prefixes);
if (\is_array($path)) {
if (null === $this->prefixes) {
$paths = $path;
} elseif ($missing = array_diff_key($this->prefixes, $path)) {
throw new \LogicException(sprintf('Route "%s" is missing routes for locale(s) "%s".', $name, implode('", "', array_keys($missing))));
} else {
foreach ($path as $locale => $localePath) {
if (!isset($this->prefixes[$locale])) {
throw new \LogicException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
}
return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes);
}
$paths[$locale] = $this->prefixes[$locale].$localePath;
}
}
} elseif (null !== $this->prefixes) {
foreach ($this->prefixes as $locale => $prefix) {
$paths[$locale] = $prefix.$path;
}
} else {
$this->collection->add($this->name.$name, $route = $this->createRoute($path));
return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes);
}
$routes = new RouteCollection();
foreach ($paths as $locale => $path) {
$routes->add($name.'.'.$locale, $route = $this->createRoute($path));
$this->collection->add($this->name.$name.'.'.$locale, $route);
$route->setDefault('_locale', $locale);
$route->setDefault('_canonical_route', $this->name.$name);
}
return new RouteConfigurator($this->collection, $routes, $this->name, $parentConfigurator, $this->prefixes);
public function alias(string $name, string $alias): AliasConfigurator
{
return new AliasConfigurator($this->collection->addAlias($name, $alias));
}
/**
@@ -78,13 +53,8 @@ trait AddTrait
*
* @param string|array $path the path, or the localized paths of the route
*/
final public function __invoke(string $name, $path): RouteConfigurator
public function __invoke(string $name, $path): RouteConfigurator
{
return $this->add($name, $path);
}
private function createRoute($path): Route
{
return new Route($path);
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Loader\Configurator\Traits;
use Symfony\Component\Routing\RouteCollection;
/**
* @internal
*/
trait HostTrait
{
final protected function addHost(RouteCollection $routes, $hosts)
{
if (!$hosts || !\is_array($hosts)) {
$routes->setHost($hosts ?: '');
return;
}
foreach ($routes->all() as $name => $route) {
if (null === $locale = $route->getDefault('_locale')) {
$routes->remove($name);
foreach ($hosts as $locale => $host) {
$localizedRoute = clone $route;
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setRequirement('_locale', preg_quote($locale));
$localizedRoute->setDefault('_canonical_route', $name);
$localizedRoute->setHost($host);
$routes->add($name.'.'.$locale, $localizedRoute);
}
} elseif (!isset($hosts[$locale])) {
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding host in its parent collection.', $name, $locale));
} else {
$route->setHost($hosts[$locale]);
$route->setRequirement('_locale', preg_quote($locale));
$routes->add($name, $route);
}
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Loader\Configurator\Traits;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* @internal
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Jules Pietri <jules@heahprod.com>
*/
trait LocalizedRouteTrait
{
/**
* Creates one or many routes.
*
* @param string|array $path the path, or the localized paths of the route
*/
final protected function createLocalizedRoute(RouteCollection $collection, string $name, $path, string $namePrefix = '', array $prefixes = null): RouteCollection
{
$paths = [];
$routes = new RouteCollection();
if (\is_array($path)) {
if (null === $prefixes) {
$paths = $path;
} elseif ($missing = array_diff_key($prefixes, $path)) {
throw new \LogicException(sprintf('Route "%s" is missing routes for locale(s) "%s".', $name, implode('", "', array_keys($missing))));
} else {
foreach ($path as $locale => $localePath) {
if (!isset($prefixes[$locale])) {
throw new \LogicException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
}
$paths[$locale] = $prefixes[$locale].$localePath;
}
}
} elseif (null !== $prefixes) {
foreach ($prefixes as $locale => $prefix) {
$paths[$locale] = $prefix.$path;
}
} else {
$routes->add($namePrefix.$name, $route = $this->createRoute($path));
$collection->add($namePrefix.$name, $route);
return $routes;
}
foreach ($paths as $locale => $path) {
$routes->add($name.'.'.$locale, $route = $this->createRoute($path));
$collection->add($namePrefix.$name.'.'.$locale, $route);
$route->setDefault('_locale', $locale);
$route->setRequirement('_locale', preg_quote($locale));
$route->setDefault('_canonical_route', $namePrefix.$name);
}
return $routes;
}
private function createRoute(string $path): Route
{
return new Route($path);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Loader\Configurator\Traits;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* @internal
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait PrefixTrait
{
final protected function addPrefix(RouteCollection $routes, $prefix, bool $trailingSlashOnRoot)
{
if (\is_array($prefix)) {
foreach ($prefix as $locale => $localePrefix) {
$prefix[$locale] = trim(trim($localePrefix), '/');
}
foreach ($routes->all() as $name => $route) {
if (null === $locale = $route->getDefault('_locale')) {
$routes->remove($name);
foreach ($prefix as $locale => $localePrefix) {
$localizedRoute = clone $route;
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setRequirement('_locale', preg_quote($locale));
$localizedRoute->setDefault('_canonical_route', $name);
$localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$routes->add($name.'.'.$locale, $localizedRoute);
}
} elseif (!isset($prefix[$locale])) {
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
} else {
$route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$routes->add($name, $route);
}
}
return;
}
$routes->addPrefix($prefix);
if (!$trailingSlashOnRoot) {
$rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath();
foreach ($routes->all() as $route) {
if ($route->getPath() === $rootPath) {
$route->setPath(rtrim($rootPath, '/'));
}
}
}
}
}

View File

@@ -19,14 +19,14 @@ trait RouteTrait
/**
* @var RouteCollection|Route
*/
private $route;
protected $route;
/**
* Adds defaults.
*
* @return $this
*/
final public function defaults(array $defaults)
final public function defaults(array $defaults): self
{
$this->route->addDefaults($defaults);
@@ -38,7 +38,7 @@ trait RouteTrait
*
* @return $this
*/
final public function requirements(array $requirements)
final public function requirements(array $requirements): self
{
$this->route->addRequirements($requirements);
@@ -50,7 +50,7 @@ trait RouteTrait
*
* @return $this
*/
final public function options(array $options)
final public function options(array $options): self
{
$this->route->addOptions($options);
@@ -62,7 +62,7 @@ trait RouteTrait
*
* @return $this
*/
final public function utf8(bool $utf8 = true)
final public function utf8(bool $utf8 = true): self
{
$this->route->addOptions(['utf8' => $utf8]);
@@ -74,7 +74,7 @@ trait RouteTrait
*
* @return $this
*/
final public function condition(string $condition)
final public function condition(string $condition): self
{
$this->route->setCondition($condition);
@@ -86,7 +86,7 @@ trait RouteTrait
*
* @return $this
*/
final public function host(string $pattern)
final public function host(string $pattern): self
{
$this->route->setHost($pattern);
@@ -101,7 +101,7 @@ trait RouteTrait
*
* @return $this
*/
final public function schemes(array $schemes)
final public function schemes(array $schemes): self
{
$this->route->setSchemes($schemes);
@@ -116,7 +116,7 @@ trait RouteTrait
*
* @return $this
*/
final public function methods(array $methods)
final public function methods(array $methods): self
{
$this->route->setMethods($methods);
@@ -126,11 +126,11 @@ trait RouteTrait
/**
* Adds the "_controller" entry to defaults.
*
* @param callable|string $controller a callable or parseable pseudo-callable
* @param callable|string|array $controller a callable or parseable pseudo-callable
*
* @return $this
*/
final public function controller($controller)
final public function controller($controller): self
{
$this->route->addDefaults(['_controller' => $controller]);
@@ -142,7 +142,7 @@ trait RouteTrait
*
* @return $this
*/
final public function locale(string $locale)
final public function locale(string $locale): self
{
$this->route->addDefaults(['_locale' => $locale]);
@@ -154,10 +154,22 @@ trait RouteTrait
*
* @return $this
*/
final public function format(string $format)
final public function format(string $format): self
{
$this->route->addDefaults(['_format' => $format]);
return $this;
}
/**
* Adds the "_stateless" entry to defaults.
*
* @return $this
*/
final public function stateless(bool $stateless = true): self
{
$this->route->addDefaults(['_stateless' => $stateless]);
return $this;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Loader;
use Psr\Container\ContainerInterface;
/**
* A route loader that executes a service from a PSR-11 container to load the routes.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class ContainerLoader extends ObjectLoader
{
private $container;
public function __construct(ContainerInterface $container, string $env = null)
{
$this->container = $container;
parent::__construct($env);
}
/**
* {@inheritdoc}
*/
public function supports($resource, string $type = null)
{
return 'service' === $type && \is_string($resource);
}
/**
* {@inheritdoc}
*/
protected function getObject(string $id)
{
return $this->container->get($id);
}
}

View File

@@ -1,38 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Loader\DependencyInjection;
use Psr\Container\ContainerInterface;
use Symfony\Component\Routing\Loader\ObjectRouteLoader;
/**
* A route loader that executes a service to load the routes.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class ServiceRouterLoader extends ObjectRouteLoader
{
/**
* @var ContainerInterface
*/
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
protected function getServiceObject($id)
{
return $this->container->get($id);
}
}

View File

@@ -20,7 +20,7 @@ class DirectoryLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function load($file, $type = null)
public function load($file, string $type = null)
{
$path = $this->locator->locate($file);
@@ -49,7 +49,7 @@ class DirectoryLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports($resource, string $type = null)
{
// only when type is forced to directory, not to conflict with AnnotationLoader

View File

@@ -24,7 +24,7 @@ class GlobFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function load($resource, $type = null)
public function load($resource, string $type = null)
{
$collection = new RouteCollection();
@@ -40,7 +40,7 @@ class GlobFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports($resource, string $type = null)
{
return 'glob' === $type;
}

View File

@@ -20,7 +20,7 @@ use Symfony\Component\Routing\RouteCollection;
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
abstract class ObjectRouteLoader extends Loader
abstract class ObjectLoader extends Loader
{
/**
* Returns the object that the method will be called on to load routes.
@@ -28,67 +28,51 @@ abstract class ObjectRouteLoader extends Loader
* For example, if your application uses a service container,
* the $id may be a service id.
*
* @param string $id
*
* @return object
*/
abstract protected function getServiceObject($id);
abstract protected function getObject(string $id);
/**
* Calls the service that will load the routes.
* Calls the object method that will load the routes.
*
* @param string $resource Some value that will resolve to a callable
* @param string $resource object_id::method
* @param string|null $type The resource type
*
* @return RouteCollection
*/
public function load($resource, $type = null)
public function load($resource, string $type = null)
{
if (!preg_match('/^[^\:]+(?:::?(?:[^\:]+))?$/', $resource)) {
throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the "service" route loader: use the format "service::method" or "service" if your service has an "__invoke" method.', $resource));
}
if (1 === substr_count($resource, ':')) {
$resource = str_replace(':', '::', $resource);
@trigger_error(sprintf('Referencing service route loaders with a single colon is deprecated since Symfony 4.1. Use %s instead.', $resource), E_USER_DEPRECATED);
if (!preg_match('/^[^\:]+(?:::(?:[^\:]+))?$/', $resource)) {
throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the %s route loader: use the format "object_id::method" or "object_id" if your object class has an "__invoke" method.', $resource, \is_string($type) ? '"'.$type.'"' : 'object'));
}
$parts = explode('::', $resource);
$serviceString = $parts[0];
$method = $parts[1] ?? '__invoke';
$loaderObject = $this->getServiceObject($serviceString);
$loaderObject = $this->getObject($parts[0]);
if (!\is_object($loaderObject)) {
throw new \LogicException(sprintf('%s:getServiceObject() must return an object: %s returned', \get_class($this), \gettype($loaderObject)));
throw new \TypeError(sprintf('"%s:getObject()" must return an object: "%s" returned.', static::class, get_debug_type($loaderObject)));
}
if (!\is_callable([$loaderObject, $method])) {
throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s"', $method, \get_class($loaderObject), $resource));
throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s".', $method, get_debug_type($loaderObject), $resource));
}
$routeCollection = $loaderObject->$method($this);
$routeCollection = $loaderObject->$method($this, $this->env);
if (!$routeCollection instanceof RouteCollection) {
$type = \is_object($routeCollection) ? \get_class($routeCollection) : \gettype($routeCollection);
$type = get_debug_type($routeCollection);
throw new \LogicException(sprintf('The %s::%s method must return a RouteCollection: %s returned', \get_class($loaderObject), $method, $type));
throw new \LogicException(sprintf('The "%s::%s()" method must return a RouteCollection: "%s" returned.', get_debug_type($loaderObject), $method, $type));
}
// make the service file tracked so that if it changes, the cache rebuilds
// make the object file tracked so that if it changes, the cache rebuilds
$this->addClassResource(new \ReflectionClass($loaderObject), $routeCollection);
return $routeCollection;
}
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
{
return 'service' === $type;
}
private function addClassResource(\ReflectionClass $class, RouteCollection $collection)
{
do {

View File

@@ -22,6 +22,8 @@ use Symfony\Component\Routing\RouteCollection;
* The file must return a RouteCollection instance.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Nicolas grekas <p@tchwork.com>
* @author Jules Pietri <jules@heahprod.com>
*/
class PhpFileLoader extends FileLoader
{
@@ -31,24 +33,23 @@ class PhpFileLoader extends FileLoader
* @param string $file A PHP file path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
* @return RouteCollection
*/
public function load($file, $type = null)
public function load($file, string $type = null)
{
$path = $this->locator->locate($file);
$this->setCurrentDir(\dirname($path));
// the closure forbids access to the private scope in the included file
$loader = $this;
$load = \Closure::bind(function ($file) use ($loader) {
$load = \Closure::bind(static function ($file) use ($loader) {
return include $file;
}, null, ProtectedPhpFileLoader::class);
$result = $load($path);
if (\is_object($result) && \is_callable($result)) {
$collection = new RouteCollection();
$result(new RoutingConfigurator($collection, $this, $path, $file));
$collection = $this->callConfigurator($result, $path, $file);
} else {
$collection = $result;
}
@@ -61,9 +62,18 @@ class PhpFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports($resource, string $type = null)
{
return \is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'php' === $type);
return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'php' === $type);
}
protected function callConfigurator(callable $result, string $path, string $file): RouteCollection
{
$collection = new RouteCollection();
$result(new RoutingConfigurator($collection, $this, $path, $file, $this->env));
return $collection;
}
}

View File

@@ -14,7 +14,9 @@ namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait;
use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait;
use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait;
use Symfony\Component\Routing\RouteCollection;
/**
@@ -25,8 +27,12 @@ use Symfony\Component\Routing\RouteCollection;
*/
class XmlFileLoader extends FileLoader
{
const NAMESPACE_URI = 'http://symfony.com/schema/routing';
const SCHEME_PATH = '/schema/routing/routing-1.0.xsd';
use HostTrait;
use LocalizedRouteTrait;
use PrefixTrait;
public const NAMESPACE_URI = 'http://symfony.com/schema/routing';
public const SCHEME_PATH = '/schema/routing/routing-1.0.xsd';
/**
* Loads an XML file.
@@ -34,12 +40,12 @@ class XmlFileLoader extends FileLoader
* @param string $file An XML file path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
* @return RouteCollection
*
* @throws \InvalidArgumentException when the file cannot be loaded or when the XML cannot be
* parsed because it does not validate against the scheme
*/
public function load($file, $type = null)
public function load($file, string $type = null)
{
$path = $this->locator->locate($file);
@@ -63,14 +69,9 @@ class XmlFileLoader extends FileLoader
/**
* Parses a node from a loaded XML file.
*
* @param RouteCollection $collection Collection to associate with the node
* @param \DOMElement $node Element to parse
* @param string $path Full path of the XML file being processed
* @param string $file Loaded file name
*
* @throws \InvalidArgumentException When the XML is invalid
*/
protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file)
protected function parseNode(RouteCollection $collection, \DOMElement $node, string $path, string $file)
{
if (self::NAMESPACE_URI !== $node->namespaceURI) {
return;
@@ -83,6 +84,16 @@ class XmlFileLoader extends FileLoader
case 'import':
$this->parseImport($collection, $node, $path, $file);
break;
case 'when':
if (!$this->env || $node->getAttribute('env') !== $this->env) {
break;
}
foreach ($node->childNodes as $node) {
if ($node instanceof \DOMElement) {
$this->parseNode($collection, $node, $path, $file);
}
}
break;
default:
throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path));
}
@@ -91,30 +102,36 @@ class XmlFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports($resource, string $type = null)
{
return \is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'xml' === $type);
return \is_string($resource) && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'xml' === $type);
}
/**
* Parses a route and adds it to the RouteCollection.
*
* @param RouteCollection $collection RouteCollection instance
* @param \DOMElement $node Element to parse that represents a Route
* @param string $path Full path of the XML file being processed
*
* @throws \InvalidArgumentException When the XML is invalid
*/
protected function parseRoute(RouteCollection $collection, \DOMElement $node, $path)
protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $path)
{
if ('' === $id = $node->getAttribute('id')) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" attribute.', $path));
}
$schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY);
$methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY);
if ('' !== $alias = $node->getAttribute('alias')) {
$alias = $collection->addAlias($id, $alias);
list($defaults, $requirements, $options, $condition, $paths) = $this->parseConfigs($node, $path);
if ($deprecationInfo = $this->parseDeprecation($node, $path)) {
$alias->setDeprecated($deprecationInfo['package'], $deprecationInfo['version'], $deprecationInfo['message']);
}
return;
}
$schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY);
$methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY);
[$defaults, $requirements, $options, $condition, $paths, /* $prefixes */, $hosts] = $this->parseConfigs($node, $path);
if (!$paths && '' === $node->getAttribute('path')) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have a "path" attribute or <path> child nodes.', $path));
@@ -124,30 +141,25 @@ class XmlFileLoader extends FileLoader
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "path" attribute and <path> child nodes.', $path));
}
if (!$paths) {
$route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition);
$collection->add($id, $route);
} else {
foreach ($paths as $locale => $p) {
$defaults['_locale'] = $locale;
$defaults['_canonical_route'] = $id;
$route = new Route($p, $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition);
$collection->add($id.'.'.$locale, $route);
}
$routes = $this->createLocalizedRoute($collection, $id, $paths ?: $node->getAttribute('path'));
$routes->addDefaults($defaults);
$routes->addRequirements($requirements);
$routes->addOptions($options);
$routes->setSchemes($schemes);
$routes->setMethods($methods);
$routes->setCondition($condition);
if (null !== $hosts) {
$this->addHost($routes, $hosts);
}
}
/**
* Parses an import and adds the routes in the resource to the RouteCollection.
*
* @param RouteCollection $collection RouteCollection instance
* @param \DOMElement $node Element to parse that represents a Route
* @param string $path Full path of the XML file being processed
* @param string $file Loaded file name
*
* @throws \InvalidArgumentException When the XML is invalid
*/
protected function parseImport(RouteCollection $collection, \DOMElement $node, $path, $file)
protected function parseImport(RouteCollection $collection, \DOMElement $node, string $path, string $file)
{
if ('' === $resource = $node->getAttribute('resource')) {
throw new \InvalidArgumentException(sprintf('The <import> element in file "%s" must have a "resource" attribute.', $path));
@@ -155,64 +167,47 @@ class XmlFileLoader extends FileLoader
$type = $node->getAttribute('type');
$prefix = $node->getAttribute('prefix');
$host = $node->hasAttribute('host') ? $node->getAttribute('host') : null;
$schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY) : null;
$methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY) : null;
$schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY) : null;
$methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY) : null;
$trailingSlashOnRoot = $node->hasAttribute('trailing-slash-on-root') ? XmlUtils::phpize($node->getAttribute('trailing-slash-on-root')) : true;
$namePrefix = $node->getAttribute('name-prefix') ?: null;
list($defaults, $requirements, $options, $condition, /* $paths */, $prefixes) = $this->parseConfigs($node, $path);
[$defaults, $requirements, $options, $condition, /* $paths */, $prefixes, $hosts] = $this->parseConfigs($node, $path);
if ('' !== $prefix && $prefixes) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "prefix" attribute and <prefix> child nodes.', $path));
}
$exclude = [];
foreach ($node->childNodes as $child) {
if ($child instanceof \DOMElement && $child->localName === $exclude && self::NAMESPACE_URI === $child->namespaceURI) {
$exclude[] = $child->nodeValue;
}
}
if ($node->hasAttribute('exclude')) {
if ($exclude) {
throw new \InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.');
}
$exclude = [$node->getAttribute('exclude')];
}
$this->setCurrentDir(\dirname($path));
/** @var RouteCollection[] $imported */
$imported = $this->import($resource, ('' !== $type ? $type : null), false, $file);
$imported = $this->import($resource, ('' !== $type ? $type : null), false, $file, $exclude) ?: [];
if (!\is_array($imported)) {
$imported = [$imported];
}
foreach ($imported as $subCollection) {
/* @var $subCollection RouteCollection */
if ('' !== $prefix || !$prefixes) {
$subCollection->addPrefix($prefix);
if (!$trailingSlashOnRoot) {
$rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath();
foreach ($subCollection->all() as $route) {
if ($route->getPath() === $rootPath) {
$route->setPath(rtrim($rootPath, '/'));
}
}
}
} else {
foreach ($prefixes as $locale => $localePrefix) {
$prefixes[$locale] = trim(trim($localePrefix), '/');
}
foreach ($subCollection->all() as $name => $route) {
if (null === $locale = $route->getDefault('_locale')) {
$subCollection->remove($name);
foreach ($prefixes as $locale => $localePrefix) {
$localizedRoute = clone $route;
$localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setDefault('_canonical_route', $name);
$subCollection->add($name.'.'.$locale, $localizedRoute);
}
} elseif (!isset($prefixes[$locale])) {
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix when imported in "%s".', $name, $locale, $path));
} else {
$route->setPath($prefixes[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$subCollection->add($name, $route);
}
}
$this->addPrefix($subCollection, $prefixes ?: $prefix, $trailingSlashOnRoot);
if (null !== $hosts) {
$this->addHost($subCollection, $hosts);
}
if (null !== $host) {
$subCollection->setHost($host);
}
if (null !== $condition) {
$subCollection->setCondition($condition);
}
@@ -222,30 +217,25 @@ class XmlFileLoader extends FileLoader
if (null !== $methods) {
$subCollection->setMethods($methods);
}
if (null !== $namePrefix) {
$subCollection->addNamePrefix($namePrefix);
}
$subCollection->addDefaults($defaults);
$subCollection->addRequirements($requirements);
$subCollection->addOptions($options);
if ($namePrefix = $node->getAttribute('name-prefix')) {
$subCollection->addNamePrefix($namePrefix);
}
$collection->addCollection($subCollection);
}
}
/**
* Loads an XML file.
*
* @param string $file An XML file path
*
* @return \DOMDocument
*
* @throws \InvalidArgumentException When loading of XML file fails because of syntax errors
* or when the XML structure is not as expected by the scheme -
* see validate()
*/
protected function loadFile($file)
protected function loadFile(string $file)
{
return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH);
}
@@ -253,14 +243,9 @@ class XmlFileLoader extends FileLoader
/**
* Parses the config elements (default, requirement, option).
*
* @param \DOMElement $node Element to parse that contains the configs
* @param string $path Full path of the XML file being processed
*
* @return array An array with the defaults as first item, requirements as second and options as third
*
* @throws \InvalidArgumentException When the XML is invalid
*/
private function parseConfigs(\DOMElement $node, $path)
private function parseConfigs(\DOMElement $node, string $path): array
{
$defaults = [];
$requirements = [];
@@ -268,6 +253,7 @@ class XmlFileLoader extends FileLoader
$condition = null;
$prefixes = [];
$paths = [];
$hosts = [];
/** @var \DOMElement $n */
foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) {
@@ -279,6 +265,9 @@ class XmlFileLoader extends FileLoader
case 'path':
$paths[$n->getAttribute('locale')] = trim($n->textContent);
break;
case 'host':
$hosts[$n->getAttribute('locale')] = trim($n->textContent);
break;
case 'prefix':
$prefixes[$n->getAttribute('locale')] = trim($n->textContent);
break;
@@ -306,9 +295,9 @@ class XmlFileLoader extends FileLoader
if ($controller = $node->getAttribute('controller')) {
if (isset($defaults['_controller'])) {
$name = $node->hasAttribute('id') ? sprintf('"%s"', $node->getAttribute('id')) : sprintf('the "%s" tag', $node->tagName);
$name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName);
throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" attribute and the defaults key "_controller" for %s.', $path, $name));
throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" attribute and the defaults key "_controller" for ', $path).$name);
}
$defaults['_controller'] = $controller;
@@ -322,22 +311,32 @@ class XmlFileLoader extends FileLoader
if ($node->hasAttribute('utf8')) {
$options['utf8'] = XmlUtils::phpize($node->getAttribute('utf8'));
}
if ($stateless = $node->getAttribute('stateless')) {
if (isset($defaults['_stateless'])) {
$name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName);
return [$defaults, $requirements, $options, $condition, $paths, $prefixes];
throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for ', $path).$name);
}
$defaults['_stateless'] = XmlUtils::phpize($stateless);
}
if (!$hosts) {
$hosts = $node->hasAttribute('host') ? $node->getAttribute('host') : null;
}
return [$defaults, $requirements, $options, $condition, $paths, $prefixes, $hosts];
}
/**
* Parses the "default" elements.
*
* @param \DOMElement $element The "default" element to parse
* @param string $path Full path of the XML file being processed
*
* @return array|bool|float|int|string|null The parsed value of the "default" element
* @return array|bool|float|int|string|null
*/
private function parseDefaultsConfig(\DOMElement $element, $path)
private function parseDefaultsConfig(\DOMElement $element, string $path)
{
if ($this->isElementValueNull($element)) {
return;
return null;
}
// Check for existing element nodes in the default element. There can
@@ -364,17 +363,14 @@ class XmlFileLoader extends FileLoader
/**
* Recursively parses the value of a "default" element.
*
* @param \DOMElement $node The node value
* @param string $path Full path of the XML file being processed
*
* @return array|bool|float|int|string The parsed value
* @return array|bool|float|int|string|null
*
* @throws \InvalidArgumentException when the XML is invalid
*/
private function parseDefaultNode(\DOMElement $node, $path)
private function parseDefaultNode(\DOMElement $node, string $path)
{
if ($this->isElementValueNull($node)) {
return;
return null;
}
switch ($node->localName) {
@@ -423,7 +419,7 @@ class XmlFileLoader extends FileLoader
}
}
private function isElementValueNull(\DOMElement $element)
private function isElementValueNull(\DOMElement $element): bool
{
$namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance';
@@ -433,4 +429,41 @@ class XmlFileLoader extends FileLoader
return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil');
}
/**
* Parses the deprecation elements.
*
* @throws \InvalidArgumentException When the XML is invalid
*/
private function parseDeprecation(\DOMElement $node, string $path): array
{
$deprecatedNode = null;
foreach ($node->childNodes as $child) {
if (!$child instanceof \DOMElement || self::NAMESPACE_URI !== $child->namespaceURI) {
continue;
}
if ('deprecated' !== $child->localName) {
throw new \InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $node->getAttribute('id'), $path));
}
$deprecatedNode = $child;
}
if (null === $deprecatedNode) {
return [];
}
if (!$deprecatedNode->hasAttribute('package')) {
throw new \InvalidArgumentException(sprintf('The <deprecated> element in file "%s" must have a "package" attribute.', $path));
}
if (!$deprecatedNode->hasAttribute('version')) {
throw new \InvalidArgumentException(sprintf('The <deprecated> element in file "%s" must have a "version" attribute.', $path));
}
return [
'package' => $deprecatedNode->getAttribute('package'),
'version' => $deprecatedNode->getAttribute('version'),
'message' => trim($deprecatedNode->nodeValue),
];
}
}

View File

@@ -13,7 +13,9 @@ namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait;
use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait;
use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser as YamlParser;
@@ -27,8 +29,12 @@ use Symfony\Component\Yaml\Yaml;
*/
class YamlFileLoader extends FileLoader
{
private static $availableKeys = [
'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', 'locale', 'format', 'utf8',
use HostTrait;
use LocalizedRouteTrait;
use PrefixTrait;
private const AVAILABLE_KEYS = [
'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', 'locale', 'format', 'utf8', 'exclude', 'stateless',
];
private $yamlParser;
@@ -38,11 +44,11 @@ class YamlFileLoader extends FileLoader
* @param string $file A Yaml file path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
* @return RouteCollection
*
* @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid
*/
public function load($file, $type = null)
public function load($file, string $type = null)
{
$path = $this->locator->locate($file);
@@ -61,7 +67,7 @@ class YamlFileLoader extends FileLoader
try {
$parsedConfig = $this->yamlParser->parseFile($path, Yaml::PARSE_CONSTANT);
} catch (ParseException $e) {
throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e);
throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML: ', $path).$e->getMessage(), 0, $e);
}
$collection = new RouteCollection();
@@ -78,6 +84,24 @@ class YamlFileLoader extends FileLoader
}
foreach ($parsedConfig as $name => $config) {
if (0 === strpos($name, 'when@')) {
if (!$this->env || 'when@'.$this->env !== $name) {
continue;
}
foreach ($config as $name => $config) {
$this->validate($config, $name.'" when "@'.$this->env, $path);
if (isset($config['resource'])) {
$this->parseImport($collection, $config, $path, $file);
} else {
$this->parseRoute($collection, $name, $config, $path);
}
}
continue;
}
$this->validate($config, $name, $path);
if (isset($config['resource'])) {
@@ -93,32 +117,37 @@ class YamlFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports($resource, string $type = null)
{
return \is_string($resource) && \in_array(pathinfo($resource, PATHINFO_EXTENSION), ['yml', 'yaml'], true) && (!$type || 'yaml' === $type);
return \is_string($resource) && \in_array(pathinfo($resource, \PATHINFO_EXTENSION), ['yml', 'yaml'], true) && (!$type || 'yaml' === $type);
}
/**
* Parses a route and adds it to the RouteCollection.
*
* @param RouteCollection $collection A RouteCollection instance
* @param string $name Route name
* @param array $config Route definition
* @param string $path Full path of the YAML file being processed
*/
protected function parseRoute(RouteCollection $collection, $name, array $config, $path)
protected function parseRoute(RouteCollection $collection, string $name, array $config, string $path)
{
$defaults = isset($config['defaults']) ? $config['defaults'] : [];
$requirements = isset($config['requirements']) ? $config['requirements'] : [];
$options = isset($config['options']) ? $config['options'] : [];
$host = isset($config['host']) ? $config['host'] : '';
$schemes = isset($config['schemes']) ? $config['schemes'] : [];
$methods = isset($config['methods']) ? $config['methods'] : [];
$condition = isset($config['condition']) ? $config['condition'] : null;
if (isset($config['alias'])) {
$alias = $collection->addAlias($name, $config['alias']);
$deprecation = $config['deprecated'] ?? null;
if (null !== $deprecation) {
$alias->setDeprecated(
$deprecation['package'],
$deprecation['version'],
$deprecation['message'] ?? ''
);
}
return;
}
$defaults = $config['defaults'] ?? [];
$requirements = $config['requirements'] ?? [];
$options = $config['options'] ?? [];
foreach ($requirements as $placeholder => $requirement) {
if (\is_int($placeholder)) {
@trigger_error(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s"?', $placeholder, $requirement, $name, $path), E_USER_DEPRECATED);
throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s"?', $placeholder, $requirement, $name, $path));
}
}
@@ -134,43 +163,40 @@ class YamlFileLoader extends FileLoader
if (isset($config['utf8'])) {
$options['utf8'] = $config['utf8'];
}
if (isset($config['stateless'])) {
$defaults['_stateless'] = $config['stateless'];
}
if (\is_array($config['path'])) {
$route = new Route('', $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
$routes = $this->createLocalizedRoute($collection, $name, $config['path']);
$routes->addDefaults($defaults);
$routes->addRequirements($requirements);
$routes->addOptions($options);
$routes->setSchemes($config['schemes'] ?? []);
$routes->setMethods($config['methods'] ?? []);
$routes->setCondition($config['condition'] ?? null);
foreach ($config['path'] as $locale => $path) {
$localizedRoute = clone $route;
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setDefault('_canonical_route', $name);
$localizedRoute->setPath($path);
$collection->add($name.'.'.$locale, $localizedRoute);
}
} else {
$route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
$collection->add($name, $route);
if (isset($config['host'])) {
$this->addHost($routes, $config['host']);
}
}
/**
* Parses an import and adds the routes in the resource to the RouteCollection.
*
* @param RouteCollection $collection A RouteCollection instance
* @param array $config Route definition
* @param string $path Full path of the YAML file being processed
* @param string $file Loaded file name
*/
protected function parseImport(RouteCollection $collection, array $config, $path, $file)
protected function parseImport(RouteCollection $collection, array $config, string $path, string $file)
{
$type = isset($config['type']) ? $config['type'] : null;
$prefix = isset($config['prefix']) ? $config['prefix'] : '';
$defaults = isset($config['defaults']) ? $config['defaults'] : [];
$requirements = isset($config['requirements']) ? $config['requirements'] : [];
$options = isset($config['options']) ? $config['options'] : [];
$host = isset($config['host']) ? $config['host'] : null;
$condition = isset($config['condition']) ? $config['condition'] : null;
$schemes = isset($config['schemes']) ? $config['schemes'] : null;
$methods = isset($config['methods']) ? $config['methods'] : null;
$type = $config['type'] ?? null;
$prefix = $config['prefix'] ?? '';
$defaults = $config['defaults'] ?? [];
$requirements = $config['requirements'] ?? [];
$options = $config['options'] ?? [];
$host = $config['host'] ?? null;
$condition = $config['condition'] ?? null;
$schemes = $config['schemes'] ?? null;
$methods = $config['methods'] ?? null;
$trailingSlashOnRoot = $config['trailing_slash_on_root'] ?? true;
$namePrefix = $config['name_prefix'] ?? null;
$exclude = $config['exclude'] ?? null;
if (isset($config['controller'])) {
$defaults['_controller'] = $config['controller'];
@@ -184,52 +210,24 @@ class YamlFileLoader extends FileLoader
if (isset($config['utf8'])) {
$options['utf8'] = $config['utf8'];
}
if (isset($config['stateless'])) {
$defaults['_stateless'] = $config['stateless'];
}
$this->setCurrentDir(\dirname($path));
$imported = $this->import($config['resource'], $type, false, $file);
/** @var RouteCollection[] $imported */
$imported = $this->import($config['resource'], $type, false, $file, $exclude) ?: [];
if (!\is_array($imported)) {
$imported = [$imported];
}
foreach ($imported as $subCollection) {
/* @var $subCollection RouteCollection */
if (!\is_array($prefix)) {
$subCollection->addPrefix($prefix);
if (!$trailingSlashOnRoot) {
$rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath();
foreach ($subCollection->all() as $route) {
if ($route->getPath() === $rootPath) {
$route->setPath(rtrim($rootPath, '/'));
}
}
}
} else {
foreach ($prefix as $locale => $localePrefix) {
$prefix[$locale] = trim(trim($localePrefix), '/');
}
foreach ($subCollection->all() as $name => $route) {
if (null === $locale = $route->getDefault('_locale')) {
$subCollection->remove($name);
foreach ($prefix as $locale => $localePrefix) {
$localizedRoute = clone $route;
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setDefault('_canonical_route', $name);
$localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$subCollection->add($name.'.'.$locale, $localizedRoute);
}
} elseif (!isset($prefix[$locale])) {
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix when imported in "%s".', $name, $locale, $file));
} else {
$route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$subCollection->add($name, $route);
}
}
}
$this->addPrefix($subCollection, $prefix, $trailingSlashOnRoot);
if (null !== $host) {
$subCollection->setHost($host);
$this->addHost($subCollection, $host);
}
if (null !== $condition) {
$subCollection->setCondition($condition);
@@ -240,14 +238,13 @@ class YamlFileLoader extends FileLoader
if (null !== $methods) {
$subCollection->setMethods($methods);
}
if (null !== $namePrefix) {
$subCollection->addNamePrefix($namePrefix);
}
$subCollection->addDefaults($defaults);
$subCollection->addRequirements($requirements);
$subCollection->addOptions($options);
if (isset($config['name_prefix'])) {
$subCollection->addNamePrefix($config['name_prefix']);
}
$collection->addCollection($subCollection);
}
}
@@ -262,13 +259,18 @@ class YamlFileLoader extends FileLoader
* @throws \InvalidArgumentException If one of the provided config keys is not supported,
* something is missing or the combination is nonsense
*/
protected function validate($config, $name, $path)
protected function validate($config, string $name, string $path)
{
if (!\is_array($config)) {
throw new \InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path));
}
if ($extraKeys = array_diff(array_keys($config), self::$availableKeys)) {
throw new \InvalidArgumentException(sprintf('The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".', $path, $name, implode('", "', $extraKeys), implode('", "', self::$availableKeys)));
if (isset($config['alias'])) {
$this->validateAlias($config, $name, $path);
return;
}
if ($extraKeys = array_diff(array_keys($config), self::AVAILABLE_KEYS)) {
throw new \InvalidArgumentException(sprintf('The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".', $path, $name, implode('", "', $extraKeys), implode('", "', self::AVAILABLE_KEYS)));
}
if (isset($config['resource']) && isset($config['path'])) {
throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "resource" key and the "path" key for "%s". Choose between an import and a route definition.', $path, $name));
@@ -282,5 +284,31 @@ class YamlFileLoader extends FileLoader
if (isset($config['controller']) && isset($config['defaults']['_controller'])) {
throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" key and the defaults key "_controller" for "%s".', $path, $name));
}
if (isset($config['stateless']) && isset($config['defaults']['_stateless'])) {
throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" key and the defaults key "_stateless" for "%s".', $path, $name));
}
}
/**
* @throws \InvalidArgumentException If one of the provided config keys is not supported,
* something is missing or the combination is nonsense
*/
private function validateAlias(array $config, string $name, string $path): void
{
foreach ($config as $key => $value) {
if (!\in_array($key, ['alias', 'deprecated'], true)) {
throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify other keys than "alias" and "deprecated" for "%s".', $path, $name));
}
if ('deprecated' === $key) {
if (!isset($value['package'])) {
throw new \InvalidArgumentException(sprintf('The routing file "%s" must specify the attribute "package" of the "deprecated" option for "%s".', $path, $name));
}
if (!isset($value['version'])) {
throw new \InvalidArgumentException(sprintf('The routing file "%s" must specify the attribute "version" of the "deprecated" option for "%s".', $path, $name));
}
}
}
}
}

View File

@@ -21,9 +21,18 @@
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="import" type="import" />
<xsd:element name="route" type="route" />
<xsd:element name="when" type="when" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="when">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="import" type="import" />
<xsd:element name="route" type="route" />
</xsd:choice>
<xsd:attribute name="env" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="localized-path">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
@@ -45,6 +54,8 @@
<xsd:sequence>
<xsd:group ref="configs" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="path" type="localized-path" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="host" type="localized-path" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="deprecated" type="deprecated" minOccurs="0" maxOccurs="1" />
</xsd:sequence>
<xsd:attribute name="id" type="xsd:string" use="required" />
<xsd:attribute name="path" type="xsd:string" />
@@ -55,15 +66,20 @@
<xsd:attribute name="locale" type="xsd:string" />
<xsd:attribute name="format" type="xsd:string" />
<xsd:attribute name="utf8" type="xsd:boolean" />
<xsd:attribute name="stateless" type="xsd:boolean" />
<xsd:attribute name="alias" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="import">
<xsd:sequence maxOccurs="unbounded" minOccurs="0">
<xsd:group ref="configs" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="prefix" type="localized-path" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="exclude" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="host" type="localized-path" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="resource" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="exclude" type="xsd:string" />
<xsd:attribute name="prefix" type="xsd:string" />
<xsd:attribute name="name-prefix" type="xsd:string" />
<xsd:attribute name="host" type="xsd:string" />
@@ -74,6 +90,7 @@
<xsd:attribute name="format" type="xsd:string" />
<xsd:attribute name="trailing-slash-on-root" type="xsd:boolean" />
<xsd:attribute name="utf8" type="xsd:boolean" />
<xsd:attribute name="stateless" type="xsd:boolean" />
</xsd:complexType>
<xsd:complexType name="default" mixed="true">
@@ -165,4 +182,13 @@
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="deprecated">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="package" type="xsd:string" use="required" />
<xsd:attribute name="version" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
</xsd:schema>

View File

@@ -26,6 +26,6 @@ class CompiledUrlMatcher extends UrlMatcher
public function __construct(array $compiledRoutes, RequestContext $context)
{
$this->context = $context;
list($this->matchHost, $this->staticRoutes, $this->regexpList, $this->dynamicRoutes, $this->checkCondition) = $compiledRoutes;
[$this->matchHost, $this->staticRoutes, $this->regexpList, $this->dynamicRoutes, $this->checkCondition] = $compiledRoutes;
}
}

View File

@@ -83,7 +83,7 @@ EOF;
$routes = $this->getRoutes();
}
list($staticRoutes, $dynamicRoutes) = $this->groupStaticRoutes($routes);
[$staticRoutes, $dynamicRoutes] = $this->groupStaticRoutes($routes);
$conditions = [null];
$compiledRoutes[] = $this->compileStaticRoutes($staticRoutes, $conditions);
@@ -91,7 +91,7 @@ EOF;
while (true) {
try {
$this->signalingException = new \RuntimeException('preg_match(): Compilation failed: regular expression is too large');
$this->signalingException = new \RuntimeException('Compilation failed: regular expression is too large');
$compiledRoutes = array_merge($compiledRoutes, $this->compileDynamicRoutes($dynamicRoutes, $matchHost, $chunkLimit, $conditions));
break;
@@ -121,7 +121,7 @@ EOF;
}
}
EOF;
$compiledRoutes[4] = $forDump ? $checkConditionCode .= ",\n" : eval('return '.$checkConditionCode.';');
$compiledRoutes[4] = $forDump ? $checkConditionCode.",\n" : eval('return '.$checkConditionCode.';');
} else {
$compiledRoutes[4] = $forDump ? " null, // \$checkCondition\n" : null;
}
@@ -131,7 +131,7 @@ EOF;
private function generateCompiledRoutes(): string
{
list($matchHost, $staticRoutes, $regexpCode, $dynamicRoutes, $checkConditionCode) = $this->getCompiledRoutes(true);
[$matchHost, $staticRoutes, $regexpCode, $dynamicRoutes, $checkConditionCode] = $this->getCompiledRoutes(true);
$code = self::export($matchHost).', // $matchHost'."\n";
@@ -186,8 +186,8 @@ EOF;
if ($hasTrailingSlash) {
$url = substr($url, 0, -1);
}
foreach ($dynamicRegex as list($hostRx, $rx, $prefix)) {
if (('' === $prefix || 0 === strpos($url, $prefix)) && preg_match($rx, $url) && (!$host || !$hostRx || preg_match($hostRx, $host))) {
foreach ($dynamicRegex as [$hostRx, $rx, $prefix]) {
if (('' === $prefix || str_starts_with($url, $prefix)) && (preg_match($rx, $url) || preg_match($rx, $url.'/')) && (!$host || !$hostRx || preg_match($hostRx, $host))) {
$dynamicRegex[] = [$hostRegex, $regex, $staticPrefix];
$dynamicRoutes->add($name, $route);
continue 2;
@@ -221,7 +221,7 @@ EOF;
foreach ($staticRoutes as $url => $routes) {
$compiledRoutes[$url] = [];
foreach ($routes as $name => list($route, $hasTrailingSlash)) {
foreach ($routes as $name => [$route, $hasTrailingSlash]) {
$compiledRoutes[$url][] = $this->compileRoute($route, $name, (!$route->compile()->getHostVariables() ? $route->getHost() : $route->compile()->getHostRegex()) ?: null, $hasTrailingSlash, false, $conditions);
}
}
@@ -242,9 +242,9 @@ EOF;
* Paths that can match two or more routes, or have user-specified conditions are put in separate switch's cases.
*
* Last but not least:
* - Because it is not possibe to mix unicode/non-unicode patterns in a single regexp, several of them can be generated.
* - Because it is not possible to mix unicode/non-unicode patterns in a single regexp, several of them can be generated.
* - The same regexp can be used several times when the logic in the switch rejects the match. When this happens, the
* matching-but-failing subpattern is blacklisted by replacing its name by "(*F)", which forces a failure-to-match.
* matching-but-failing subpattern is excluded by replacing its name by "(*F)", which forces a failure-to-match.
* To ease this backlisting operation, the name of subpatterns is also the string offset where the replacement should occur.
*/
private function compileDynamicRoutes(RouteCollection $collection, bool $matchHost, int $chunkLimit, array &$conditions): array
@@ -287,7 +287,7 @@ EOF;
$routes->add($name, $route);
}
foreach ($perModifiers as list($modifiers, $routes)) {
foreach ($perModifiers as [$modifiers, $routes]) {
$prev = false;
$perHost = [];
foreach ($routes->all() as $name => $route) {
@@ -306,7 +306,7 @@ EOF;
$state->mark += \strlen($rx);
$state->regex = $rx;
foreach ($perHost as list($hostRegex, $routes)) {
foreach ($perHost as [$hostRegex, $routes]) {
if ($matchHost) {
if ($hostRegex) {
preg_match('#^.\^(.*)\$.[a-zA-Z]*$#', $hostRegex, $rx);
@@ -349,7 +349,7 @@ EOF;
$state->markTail = 0;
// if the regex is too large, throw a signaling exception to recompute with smaller chunk size
set_error_handler(function ($type, $message) { throw 0 === strpos($message, $this->signalingException->getMessage()) ? $this->signalingException : new \ErrorException($message); });
set_error_handler(function ($type, $message) { throw str_contains($message, $this->signalingException->getMessage()) ? $this->signalingException : new \ErrorException($message); });
try {
preg_match($state->regex, '');
} finally {
@@ -391,7 +391,7 @@ EOF;
continue;
}
list($name, $regex, $vars, $route, $hasTrailingSlash, $hasTrailingVar) = $route;
[$name, $regex, $vars, $route, $hasTrailingSlash, $hasTrailingVar] = $route;
$compiledRoute = $route->compile();
$vars = array_merge($state->hostVars, $vars);
@@ -427,7 +427,7 @@ EOF;
if ($condition = $route->getCondition()) {
$condition = $this->getExpressionLanguage()->compile($condition, ['context', 'request']);
$condition = $conditions[$condition] ?? $conditions[$condition] = (false !== strpos($condition, '$request') ? 1 : -1) * \count($conditions);
$condition = $conditions[$condition] ?? $conditions[$condition] = (str_contains($condition, '$request') ? 1 : -1) * \count($conditions);
} else {
$condition = null;
}
@@ -443,10 +443,10 @@ EOF;
];
}
private function getExpressionLanguage()
private function getExpressionLanguage(): ExpressionLanguage
{
if (null === $this->expressionLanguage) {
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
if (!class_exists(ExpressionLanguage::class)) {
throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
}
$this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
@@ -455,7 +455,7 @@ EOF;
return $this->expressionLanguage;
}
private function indent($code, $level = 1)
private function indent(string $code, int $level = 1): string
{
return preg_replace('/^./m', str_repeat(' ', $level).'$0', $code);
}

View File

@@ -30,9 +30,13 @@ trait CompiledUrlMatcherTrait
private $staticRoutes = [];
private $regexpList = [];
private $dynamicRoutes = [];
/**
* @var callable|null
*/
private $checkCondition;
public function match($pathinfo)
public function match(string $pathinfo): array
{
$allow = $allowSchemes = [];
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
@@ -87,16 +91,16 @@ trait CompiledUrlMatcherTrait
}
$supportsRedirections = 'GET' === $canonicalMethod && $this instanceof RedirectableUrlMatcherInterface;
foreach ($this->staticRoutes[$trimmedPathinfo] ?? [] as list($ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash, , $condition)) {
foreach ($this->staticRoutes[$trimmedPathinfo] ?? [] as [$ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash, , $condition]) {
if ($condition && !($this->checkCondition)($condition, $context, 0 < $condition ? $request ?? $request = $this->request ?: $this->createRequest($pathinfo) : null)) {
continue;
}
if ($requiredHost) {
if ('#' !== $requiredHost[0] ? $requiredHost !== $host : !preg_match($requiredHost, $host, $hostMatches)) {
if ('{' !== $requiredHost[0] ? $requiredHost !== $host : !preg_match($requiredHost, $host, $hostMatches)) {
continue;
}
if ('#' === $requiredHost[0] && $hostMatches) {
if ('{' === $requiredHost[0] && $hostMatches) {
$hostMatches['_route'] = $ret['_route'];
$ret = $this->mergeDefaults($hostMatches, $ret);
}
@@ -127,7 +131,7 @@ trait CompiledUrlMatcherTrait
foreach ($this->regexpList as $offset => $regex) {
while (preg_match($regex, $matchedPathinfo, $matches)) {
foreach ($this->dynamicRoutes[$m = (int) $matches['MARK']] as list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash, $hasTrailingVar, $condition)) {
foreach ($this->dynamicRoutes[$m = (int) $matches['MARK']] as [$ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash, $hasTrailingVar, $condition]) {
if (null !== $condition) {
if (0 === $condition) { // marks the last route in the regexp
continue 3;

View File

@@ -24,16 +24,14 @@ interface MatcherDumperInterface
* Dumps a set of routes to a string representation of executable code
* that can then be used to match a request against these routes.
*
* @param array $options An array of options
*
* @return string Executable code
* @return string
*/
public function dump(array $options = []);
/**
* Gets the routes to dump.
*
* @return RouteCollection A RouteCollection instance
* @return RouteCollection
*/
public function getRoutes();
}

View File

@@ -1,75 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Matcher\Dumper;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "CompiledUrlMatcherDumper" instead.', PhpMatcherDumper::class), E_USER_DEPRECATED);
/**
* PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Tobias Schultze <http://tobion.de>
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*
* @deprecated since Symfony 4.2, use CompiledUrlMatcherDumper instead.
*/
class PhpMatcherDumper extends CompiledUrlMatcherDumper
{
/**
* Dumps a set of routes to a PHP class.
*
* Available options:
*
* * class: The class name
* * base_class: The base class name
*
* @param array $options An array of options
*
* @return string A PHP class representing the matcher class
*/
public function dump(array $options = [])
{
$options = array_replace([
'class' => 'ProjectUrlMatcher',
'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
], $options);
$code = parent::dump();
$code = preg_replace('#\n ([^ ].*?) // \$(\w++)$#m', "\n \$this->$2 = $1", $code);
$code = str_replace(",\n $", ";\n $", $code);
$code = substr($code, strpos($code, '$this') - 4, -5).";\n";
$code = preg_replace('/^ \$this->\w++ = (?:null|false|\[\n \]);\n/m', '', $code);
$code = str_replace("\n ", "\n ", "\n".$code);
return <<<EOF
<?php
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherTrait;
use Symfony\Component\Routing\RequestContext;
/**
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class {$options['class']} extends {$options['base_class']}
{
use CompiledUrlMatcherTrait;
public function __construct(RequestContext \$context)
{
\$this->context = \$context;{$code} }
}
EOF;
}
}

View File

@@ -65,12 +65,12 @@ class StaticPrefixCollection
*/
public function addRoute(string $prefix, $route)
{
list($prefix, $staticPrefix) = $this->getCommonPrefix($prefix, $prefix);
[$prefix, $staticPrefix] = $this->getCommonPrefix($prefix, $prefix);
for ($i = \count($this->items) - 1; 0 <= $i; --$i) {
$item = $this->items[$i];
list($commonPrefix, $commonStaticPrefix) = $this->getCommonPrefix($prefix, $this->prefixes[$i]);
[$commonPrefix, $commonStaticPrefix] = $this->getCommonPrefix($prefix, $this->prefixes[$i]);
if ($this->prefix === $commonPrefix) {
// the new route and a previous one have no common prefix, let's see if they are exclusive to each others
@@ -104,8 +104,8 @@ class StaticPrefixCollection
} else {
// the new route and a previous one have a common prefix, let's merge them
$child = new self($commonPrefix);
list($child->prefixes[0], $child->staticPrefixes[0]) = $child->getCommonPrefix($this->prefixes[$i], $this->prefixes[$i]);
list($child->prefixes[1], $child->staticPrefixes[1]) = $child->getCommonPrefix($prefix, $prefix);
[$child->prefixes[0], $child->staticPrefixes[0]] = $child->getCommonPrefix($this->prefixes[$i], $this->prefixes[$i]);
[$child->prefixes[1], $child->staticPrefixes[1]] = $child->getCommonPrefix($prefix, $prefix);
$child->items = [$this->items[$i], $route];
$this->staticPrefixes[$i] = $commonStaticPrefix;
@@ -151,40 +151,43 @@ class StaticPrefixCollection
$staticLength = null;
set_error_handler([__CLASS__, 'handleError']);
for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) {
if ('(' === $prefix[$i]) {
$staticLength = $staticLength ?? $i;
for ($j = 1 + $i, $n = 1; $j < $end && 0 < $n; ++$j) {
if ($prefix[$j] !== $anotherPrefix[$j]) {
break 2;
try {
for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) {
if ('(' === $prefix[$i]) {
$staticLength = $staticLength ?? $i;
for ($j = 1 + $i, $n = 1; $j < $end && 0 < $n; ++$j) {
if ($prefix[$j] !== $anotherPrefix[$j]) {
break 2;
}
if ('(' === $prefix[$j]) {
++$n;
} elseif (')' === $prefix[$j]) {
--$n;
} elseif ('\\' === $prefix[$j] && (++$j === $end || $prefix[$j] !== $anotherPrefix[$j])) {
--$j;
break;
}
}
if ('(' === $prefix[$j]) {
++$n;
} elseif (')' === $prefix[$j]) {
--$n;
} elseif ('\\' === $prefix[$j] && (++$j === $end || $prefix[$j] !== $anotherPrefix[$j])) {
--$j;
if (0 < $n) {
break;
}
}
if (0 < $n) {
if (('?' === ($prefix[$j] ?? '') || '?' === ($anotherPrefix[$j] ?? '')) && ($prefix[$j] ?? '') !== ($anotherPrefix[$j] ?? '')) {
break;
}
$subPattern = substr($prefix, $i, $j - $i);
if ($prefix !== $anotherPrefix && !preg_match('/^\(\[[^\]]++\]\+\+\)$/', $subPattern) && !preg_match('{(?<!'.$subPattern.')}', '')) {
// sub-patterns of variable length are not considered as common prefixes because their greediness would break in-order matching
break;
}
$i = $j - 1;
} elseif ('\\' === $prefix[$i] && (++$i === $end || $prefix[$i] !== $anotherPrefix[$i])) {
--$i;
break;
}
if (('?' === ($prefix[$j] ?? '') || '?' === ($anotherPrefix[$j] ?? '')) && ($prefix[$j] ?? '') !== ($anotherPrefix[$j] ?? '')) {
break;
}
$subPattern = substr($prefix, $i, $j - $i);
if ($prefix !== $anotherPrefix && !preg_match('/^\(\[[^\]]++\]\+\+\)$/', $subPattern) && !preg_match('{(?<!'.$subPattern.')}', '')) {
// sub-patterns of variable length are not considered as common prefixes because their greediness would break in-order matching
break;
}
$i = $j - 1;
} elseif ('\\' === $prefix[$i] && (++$i === $end || $prefix[$i] !== $anotherPrefix[$i])) {
--$i;
break;
}
} finally {
restore_error_handler();
}
restore_error_handler();
if ($i < $end && 0b10 === (\ord($prefix[$i]) >> 6) && preg_match('//u', $prefix.' '.$anotherPrefix)) {
do {
// Prevent cutting in the middle of an UTF-8 characters
@@ -195,8 +198,8 @@ class StaticPrefixCollection
return [substr($prefix, 0, $i), substr($prefix, 0, $staticLength ?? $i)];
}
public static function handleError($type, $msg)
public static function handleError(int $type, string $msg)
{
return 0 === strpos($msg, 'preg_match(): Compilation failed: lookbehind assertion is not fixed length');
return str_contains($msg, 'Compilation failed: lookbehind assertion is not fixed length');
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Matcher;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Contracts\Service\ServiceProviderInterface;
/**
* Exposes functions defined in the request context to route conditions.
*
* @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com>
*/
class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface
{
private $functions;
public function __construct(ServiceProviderInterface $functions)
{
$this->functions = $functions;
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
$functions = [];
foreach ($this->functions->getProvidedServices() as $function => $type) {
$functions[] = new ExpressionFunction(
$function,
static function (...$args) use ($function) {
return sprintf('($context->getParameter(\'_functions\')->get(%s)(%s))', var_export($function, true), implode(', ', $args));
},
function ($values, ...$args) use ($function) {
return $values['context']->getParameter('_functions')->get($function)(...$args);
}
);
}
return $functions;
}
public function get(string $function): callable
{
return $this->functions->get($function);
}
}

View File

@@ -22,7 +22,7 @@ abstract class RedirectableUrlMatcher extends UrlMatcher implements Redirectable
/**
* {@inheritdoc}
*/
public function match($pathinfo)
public function match(string $pathinfo)
{
try {
return parent::match($pathinfo);

View File

@@ -19,13 +19,13 @@ namespace Symfony\Component\Routing\Matcher;
interface RedirectableUrlMatcherInterface
{
/**
* Redirects the user to another URL.
* Redirects the user to another URL and returns the parameters for the redirection.
*
* @param string $path The path info to redirect to
* @param string $route The route name that matched
* @param string|null $scheme The URL scheme (null to keep the current one)
*
* @return array An array of parameters
* @return array
*/
public function redirect($path, $route, $scheme = null);
public function redirect(string $path, string $route, string $scheme = null);
}

View File

@@ -26,10 +26,10 @@ interface RequestMatcherInterface
/**
* Tries to match a request with a set of routes.
*
* If the matcher can not find information, it must throw one of the exceptions documented
* If the matcher cannot find information, it must throw one of the exceptions documented
* below.
*
* @return array An array of parameters
* @return array
*
* @throws NoConfigurationException If no routing configuration could be found
* @throws ResourceNotFoundException If no matching resource could be found

View File

@@ -23,13 +23,13 @@ use Symfony\Component\Routing\RouteCollection;
*/
class TraceableUrlMatcher extends UrlMatcher
{
const ROUTE_DOES_NOT_MATCH = 0;
const ROUTE_ALMOST_MATCHES = 1;
const ROUTE_MATCHES = 2;
public const ROUTE_DOES_NOT_MATCH = 0;
public const ROUTE_ALMOST_MATCHES = 1;
public const ROUTE_MATCHES = 2;
protected $traces;
public function getTraces($pathinfo)
public function getTraces(string $pathinfo)
{
$this->traces = [];
@@ -50,12 +50,32 @@ class TraceableUrlMatcher extends UrlMatcher
return $traces;
}
protected function matchCollection($pathinfo, RouteCollection $routes)
protected function matchCollection(string $pathinfo, RouteCollection $routes)
{
// HEAD and GET are equivalent as per RFC
if ('HEAD' === $method = $this->context->getMethod()) {
$method = 'GET';
}
$supportsTrailingSlash = 'GET' === $method && $this instanceof RedirectableUrlMatcherInterface;
$trimmedPathinfo = rtrim($pathinfo, '/') ?: '/';
foreach ($routes as $name => $route) {
$compiledRoute = $route->compile();
$staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/');
$requiredMethods = $route->getMethods();
if (!preg_match($compiledRoute->getRegex(), $pathinfo, $matches)) {
// check the static prefix of the URL first. Only use the more expensive preg_match when it matches
if ('' !== $staticPrefix && !str_starts_with($trimmedPathinfo, $staticPrefix)) {
$this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route);
continue;
}
$regex = $compiledRoute->getRegex();
$pos = strrpos($regex, '$');
$hasTrailingSlash = '/' === $regex[$pos - 1];
$regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash);
if (!preg_match($regex, $pathinfo, $matches)) {
// does it match without any requirements?
$r = new Route($route->getPath(), $route->getDefaults(), [], $route->getOptions());
$cr = $r->compile();
@@ -79,57 +99,60 @@ class TraceableUrlMatcher extends UrlMatcher
continue;
}
// check host requirement
$hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{\w+\}/?$#', $route->getPath());
if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[\count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) {
if ($hasTrailingSlash) {
$matches = $m;
} else {
$hasTrailingVar = false;
}
}
$hostMatches = [];
if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
$this->addTrace(sprintf('Host "%s" does not match the requirement ("%s")', $this->context->getHost(), $route->getHost()), self::ROUTE_ALMOST_MATCHES, $name, $route);
continue;
}
// check HTTP method requirement
if ($requiredMethods = $route->getMethods()) {
// HEAD and GET are equivalent as per RFC
if ('HEAD' === $method = $this->context->getMethod()) {
$method = 'GET';
}
$status = $this->handleRouteRequirements($pathinfo, $name, $route);
if (!\in_array($method, $requiredMethods)) {
$this->allow = array_merge($this->allow, $requiredMethods);
$this->addTrace(sprintf('Method "%s" does not match any of the required methods (%s)', $this->context->getMethod(), implode(', ', $requiredMethods)), self::ROUTE_ALMOST_MATCHES, $name, $route);
continue;
}
if (self::REQUIREMENT_MISMATCH === $status[0]) {
$this->addTrace(sprintf('Condition "%s" does not evaluate to "true"', $route->getCondition()), self::ROUTE_ALMOST_MATCHES, $name, $route);
continue;
}
// check condition
if ($condition = $route->getCondition()) {
if (!$this->getExpressionLanguage()->evaluate($condition, ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) {
$this->addTrace(sprintf('Condition "%s" does not evaluate to "true"', $condition), self::ROUTE_ALMOST_MATCHES, $name, $route);
if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) {
$this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route);
continue;
return $this->allow = $this->allowSchemes = [];
}
$this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route);
continue;
}
// check HTTP scheme requirement
if ($requiredSchemes = $route->getSchemes()) {
$scheme = $this->context->getScheme();
if ($route->getSchemes() && !$route->hasScheme($this->context->getScheme())) {
$this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes());
$this->addTrace(sprintf('Scheme "%s" does not match any of the required schemes (%s)', $this->context->getScheme(), implode(', ', $route->getSchemes())), self::ROUTE_ALMOST_MATCHES, $name, $route);
continue;
}
if (!$route->hasScheme($scheme)) {
$this->addTrace(sprintf('Scheme "%s" does not match any of the required schemes (%s); the user will be redirected to first required scheme', $scheme, implode(', ', $requiredSchemes)), self::ROUTE_ALMOST_MATCHES, $name, $route);
return true;
}
if ($requiredMethods && !\in_array($method, $requiredMethods)) {
$this->allow = array_merge($this->allow, $requiredMethods);
$this->addTrace(sprintf('Method "%s" does not match any of the required methods (%s)', $this->context->getMethod(), implode(', ', $requiredMethods)), self::ROUTE_ALMOST_MATCHES, $name, $route);
continue;
}
$this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route);
return true;
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, $status[1] ?? []));
}
return [];
}
private function addTrace($log, $level = self::ROUTE_DOES_NOT_MATCH, $name = null, $route = null)
private function addTrace(string $log, int $level = self::ROUTE_DOES_NOT_MATCH, string $name = null, Route $route = null)
{
$this->traces[] = [
'log' => $log,

View File

@@ -28,9 +28,9 @@ use Symfony\Component\Routing\RouteCollection;
*/
class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
{
const REQUIREMENT_MATCH = 0;
const REQUIREMENT_MISMATCH = 1;
const ROUTE_MATCH = 2;
public const REQUIREMENT_MATCH = 0;
public const REQUIREMENT_MISMATCH = 1;
public const ROUTE_MATCH = 2;
/** @var RequestContext */
protected $context;
@@ -81,7 +81,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
/**
* {@inheritdoc}
*/
public function match($pathinfo)
public function match(string $pathinfo)
{
$this->allow = $this->allowSchemes = [];
@@ -93,9 +93,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
throw new NoConfigurationException();
}
throw 0 < \count($this->allow)
? new MethodNotAllowedException(array_unique($this->allow))
: new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
throw 0 < \count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
}
/**
@@ -120,16 +118,15 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
/**
* Tries to match a URL with a set of routes.
*
* @param string $pathinfo The path info to be parsed
* @param RouteCollection $routes The set of routes
* @param string $pathinfo The path info to be parsed
*
* @return array An array of parameters
* @return array
*
* @throws NoConfigurationException If no routing configuration could be found
* @throws ResourceNotFoundException If the resource could not be found
* @throws MethodNotAllowedException If the resource was found but the request method is not allowed
*/
protected function matchCollection($pathinfo, RouteCollection $routes)
protected function matchCollection(string $pathinfo, RouteCollection $routes)
{
// HEAD and GET are equivalent as per RFC
if ('HEAD' === $method = $this->context->getMethod()) {
@@ -144,7 +141,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
$requiredMethods = $route->getMethods();
// check the static prefix of the URL first. Only use the more expensive preg_match when it matches
if ('' !== $staticPrefix && 0 !== strpos($trimmedPathinfo, $staticPrefix)) {
if ('' !== $staticPrefix && !str_starts_with($trimmedPathinfo, $staticPrefix)) {
continue;
}
$regex = $compiledRoute->getRegex();
@@ -195,7 +192,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
continue;
}
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : []));
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, $status[1] ?? []));
}
return [];
@@ -208,13 +205,9 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
* in matchers that do not have access to the matched Route instance
* (like the PHP and Apache matcher dumpers).
*
* @param Route $route The route we are matching against
* @param string $name The name of the route
* @param array $attributes An array of attributes from the matcher
*
* @return array An array of parameters
* @return array
*/
protected function getAttributes(Route $route, $name, array $attributes)
protected function getAttributes(Route $route, string $name, array $attributes)
{
$defaults = $route->getDefaults();
if (isset($defaults['_canonical_route'])) {
@@ -229,13 +222,9 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
/**
* Handles specific route requirements.
*
* @param string $pathinfo The path
* @param string $name The route name
* @param Route $route The route
*
* @return array The first element represents the status, the second contains additional information
*/
protected function handleRouteRequirements($pathinfo, $name, Route $route)
protected function handleRouteRequirements(string $pathinfo, string $name, Route $route)
{
// expression condition
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) {
@@ -248,12 +237,9 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
/**
* Get merged default parameters.
*
* @param array $params The parameters
* @param array $defaults The defaults
*
* @return array Merged default parameters
* @return array
*/
protected function mergeDefaults($params, $defaults)
protected function mergeDefaults(array $params, array $defaults)
{
foreach ($params as $key => $value) {
if (!\is_int($key) && null !== $value) {
@@ -267,7 +253,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
protected function getExpressionLanguage()
{
if (null === $this->expressionLanguage) {
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
if (!class_exists(ExpressionLanguage::class)) {
throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
}
$this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
@@ -279,9 +265,9 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
/**
* @internal
*/
protected function createRequest($pathinfo)
protected function createRequest(string $pathinfo): ?Request
{
if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
if (!class_exists(Request::class)) {
return null;
}

View File

@@ -26,16 +26,16 @@ interface UrlMatcherInterface extends RequestContextAwareInterface
/**
* Tries to match a URL path with a set of routes.
*
* If the matcher can not find information, it must throw one of the exceptions documented
* If the matcher cannot find information, it must throw one of the exceptions documented
* below.
*
* @param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded)
*
* @return array An array of parameters
* @return array
*
* @throws NoConfigurationException If no routing configuration could be found
* @throws ResourceNotFoundException If the resource could not be found
* @throws MethodNotAllowedException If the resource was found but the request method is not allowed
*/
public function match($pathinfo);
public function match(string $pathinfo);
}

View File

@@ -3,11 +3,49 @@ Routing Component
The Routing component maps an HTTP request to a set of configuration variables.
Getting Started
---------------
```
$ composer require symfony/routing
```
```php
use App\Controller\BlogController;
use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
$route = new Route('/blog/{slug}', ['_controller' => BlogController::class]);
$routes = new RouteCollection();
$routes->add('blog_show', $route);
$context = new RequestContext();
// Routing can match routes with incoming requests
$matcher = new UrlMatcher($routes, $context);
$parameters = $matcher->match('/blog/lorem-ipsum');
// $parameters = [
// '_controller' => 'App\Controller\BlogController',
// 'slug' => 'lorem-ipsum',
// '_route' => 'blog_show'
// ]
// Routing can also generate URLs for a given route
$generator = new UrlGenerator($routes, $context);
$url = $generator->generate('blog_show', [
'slug' => 'my-blog-post',
]);
// $url = '/blog/my-blog-post'
```
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/routing/index.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
* [Documentation](https://symfony.com/doc/current/routing.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -45,6 +45,23 @@ class RequestContext
$this->setQueryString($queryString);
}
public static function fromUri(string $uri, string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443): self
{
$uri = parse_url($uri);
$scheme = $uri['scheme'] ?? $scheme;
$host = $uri['host'] ?? $host;
if (isset($uri['port'])) {
if ('http' === $scheme) {
$httpPort = $uri['port'];
} elseif ('https' === $scheme) {
$httpsPort = $uri['port'];
}
}
return new self($uri['path'] ?? '', 'GET', $host, $scheme, $httpPort, $httpsPort);
}
/**
* Updates the RequestContext information based on a HttpFoundation Request.
*
@@ -57,8 +74,8 @@ class RequestContext
$this->setMethod($request->getMethod());
$this->setHost($request->getHost());
$this->setScheme($request->getScheme());
$this->setHttpPort($request->isSecure() ? $this->httpPort : $request->getPort());
$this->setHttpsPort($request->isSecure() ? $request->getPort() : $this->httpsPort);
$this->setHttpPort($request->isSecure() || null === $request->getPort() ? $this->httpPort : $request->getPort());
$this->setHttpsPort($request->isSecure() && null !== $request->getPort() ? $request->getPort() : $this->httpsPort);
$this->setQueryString($request->server->get('QUERY_STRING', ''));
return $this;
@@ -67,7 +84,7 @@ class RequestContext
/**
* Gets the base URL.
*
* @return string The base URL
* @return string
*/
public function getBaseUrl()
{
@@ -77,11 +94,9 @@ class RequestContext
/**
* Sets the base URL.
*
* @param string $baseUrl The base URL
*
* @return $this
*/
public function setBaseUrl($baseUrl)
public function setBaseUrl(string $baseUrl)
{
$this->baseUrl = $baseUrl;
@@ -91,7 +106,7 @@ class RequestContext
/**
* Gets the path info.
*
* @return string The path info
* @return string
*/
public function getPathInfo()
{
@@ -101,11 +116,9 @@ class RequestContext
/**
* Sets the path info.
*
* @param string $pathInfo The path info
*
* @return $this
*/
public function setPathInfo($pathInfo)
public function setPathInfo(string $pathInfo)
{
$this->pathInfo = $pathInfo;
@@ -117,7 +130,7 @@ class RequestContext
*
* The method is always an uppercased string.
*
* @return string The HTTP method
* @return string
*/
public function getMethod()
{
@@ -127,11 +140,9 @@ class RequestContext
/**
* Sets the HTTP method.
*
* @param string $method The HTTP method
*
* @return $this
*/
public function setMethod($method)
public function setMethod(string $method)
{
$this->method = strtoupper($method);
@@ -143,7 +154,7 @@ class RequestContext
*
* The host is always lowercased because it must be treated case-insensitive.
*
* @return string The HTTP host
* @return string
*/
public function getHost()
{
@@ -153,11 +164,9 @@ class RequestContext
/**
* Sets the HTTP host.
*
* @param string $host The HTTP host
*
* @return $this
*/
public function setHost($host)
public function setHost(string $host)
{
$this->host = strtolower($host);
@@ -167,7 +176,7 @@ class RequestContext
/**
* Gets the HTTP scheme.
*
* @return string The HTTP scheme
* @return string
*/
public function getScheme()
{
@@ -177,11 +186,9 @@ class RequestContext
/**
* Sets the HTTP scheme.
*
* @param string $scheme The HTTP scheme
*
* @return $this
*/
public function setScheme($scheme)
public function setScheme(string $scheme)
{
$this->scheme = strtolower($scheme);
@@ -191,7 +198,7 @@ class RequestContext
/**
* Gets the HTTP port.
*
* @return int The HTTP port
* @return int
*/
public function getHttpPort()
{
@@ -201,13 +208,11 @@ class RequestContext
/**
* Sets the HTTP port.
*
* @param int $httpPort The HTTP port
*
* @return $this
*/
public function setHttpPort($httpPort)
public function setHttpPort(int $httpPort)
{
$this->httpPort = (int) $httpPort;
$this->httpPort = $httpPort;
return $this;
}
@@ -215,7 +220,7 @@ class RequestContext
/**
* Gets the HTTPS port.
*
* @return int The HTTPS port
* @return int
*/
public function getHttpsPort()
{
@@ -225,21 +230,19 @@ class RequestContext
/**
* Sets the HTTPS port.
*
* @param int $httpsPort The HTTPS port
*
* @return $this
*/
public function setHttpsPort($httpsPort)
public function setHttpsPort(int $httpsPort)
{
$this->httpsPort = (int) $httpsPort;
$this->httpsPort = $httpsPort;
return $this;
}
/**
* Gets the query string.
* Gets the query string without the "?".
*
* @return string The query string without the "?"
* @return string
*/
public function getQueryString()
{
@@ -249,11 +252,9 @@ class RequestContext
/**
* Sets the query string.
*
* @param string $queryString The query string (after "?")
*
* @return $this
*/
public function setQueryString($queryString)
public function setQueryString(?string $queryString)
{
// string cast to be fault-tolerant, accepting null
$this->queryString = (string) $queryString;
@@ -264,7 +265,7 @@ class RequestContext
/**
* Returns the parameters.
*
* @return array The parameters
* @return array
*/
public function getParameters()
{
@@ -288,23 +289,19 @@ class RequestContext
/**
* Gets a parameter value.
*
* @param string $name A parameter name
*
* @return mixed The parameter value or null if nonexistent
* @return mixed
*/
public function getParameter($name)
public function getParameter(string $name)
{
return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
return $this->parameters[$name] ?? null;
}
/**
* Checks if a parameter value is set for the given parameter.
*
* @param string $name A parameter name
*
* @return bool True if the parameter value is set, false otherwise
* @return bool
*/
public function hasParameter($name)
public function hasParameter(string $name)
{
return \array_key_exists($name, $this->parameters);
}
@@ -312,15 +309,19 @@ class RequestContext
/**
* Sets a parameter value.
*
* @param string $name A parameter name
* @param mixed $parameter The parameter value
* @param mixed $parameter The parameter value
*
* @return $this
*/
public function setParameter($name, $parameter)
public function setParameter(string $name, $parameter)
{
$this->parameters[$name] = $parameter;
return $this;
}
public function isSecure(): bool
{
return 'https' === $this->scheme;
}
}

View File

@@ -21,7 +21,7 @@ interface RequestContextAwareInterface
/**
* Gets the request context.
*
* @return RequestContext The context
* @return RequestContext
*/
public function getContext();
}

View File

@@ -45,10 +45,10 @@ class Route implements \Serializable
* @param array $defaults An array of default parameter values
* @param array $requirements An array of requirements for parameters (regexes)
* @param array $options An array of options
* @param string $host The host pattern to match
* @param string|null $host The host pattern to match
* @param string|string[] $schemes A required URI scheme or an array of restricted schemes
* @param string|string[] $methods A required HTTP method or an array of restricted methods
* @param string $condition A condition that should evaluate to true for the route to match
* @param string|null $condition A condition that should evaluate to true for the route to match
*/
public function __construct(string $path, array $defaults = [], array $requirements = [], array $options = [], ?string $host = '', $schemes = [], $methods = [], ?string $condition = '')
{
@@ -78,10 +78,9 @@ class Route implements \Serializable
}
/**
* @internal since Symfony 4.3
* @final since Symfony 4.3
* @internal
*/
public function serialize()
final public function serialize(): string
{
return serialize($this->__serialize());
}
@@ -105,18 +104,15 @@ class Route implements \Serializable
}
/**
* @internal since Symfony 4.3
* @final since Symfony 4.3
* @internal
*/
public function unserialize($serialized)
final public function unserialize($serialized)
{
$this->__unserialize(unserialize($serialized));
}
/**
* Returns the pattern for the path.
*
* @return string The path pattern
* @return string
*/
public function getPath()
{
@@ -124,28 +120,11 @@ class Route implements \Serializable
}
/**
* Sets the pattern for the path.
*
* This method implements a fluent interface.
*
* @param string $pattern The path pattern
*
* @return $this
*/
public function setPath($pattern)
public function setPath(string $pattern)
{
if (false !== strpbrk($pattern, '?<')) {
$pattern = preg_replace_callback('#\{(\w++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) {
if (isset($m[3][0])) {
$this->setDefault($m[1], '?' !== $m[3] ? substr($m[3], 1) : null);
}
if (isset($m[2][0])) {
$this->setRequirement($m[1], substr($m[2], 1, -1));
}
return '{'.$m[1].'}';
}, $pattern);
}
$pattern = $this->extractInlineDefaultsAndRequirements($pattern);
// A pattern must start with a slash and must not have multiple slashes at the beginning because the
// generated path for this route would be confused with a network path, e.g. '//domain.com/path'.
@@ -156,9 +135,7 @@ class Route implements \Serializable
}
/**
* Returns the pattern for the host.
*
* @return string The host pattern
* @return string
*/
public function getHost()
{
@@ -166,17 +143,11 @@ class Route implements \Serializable
}
/**
* Sets the pattern for the host.
*
* This method implements a fluent interface.
*
* @param string $pattern The host pattern
*
* @return $this
*/
public function setHost($pattern)
public function setHost(?string $pattern)
{
$this->host = (string) $pattern;
$this->host = $this->extractInlineDefaultsAndRequirements((string) $pattern);
$this->compiled = null;
return $this;
@@ -186,7 +157,7 @@ class Route implements \Serializable
* Returns the lowercased schemes this route is restricted to.
* So an empty array means that any scheme is allowed.
*
* @return string[] The schemes
* @return string[]
*/
public function getSchemes()
{
@@ -197,8 +168,6 @@ class Route implements \Serializable
* Sets the schemes (e.g. 'https') this route is restricted to.
* So an empty array means that any scheme is allowed.
*
* This method implements a fluent interface.
*
* @param string|string[] $schemes The scheme or an array of schemes
*
* @return $this
@@ -214,11 +183,9 @@ class Route implements \Serializable
/**
* Checks if a scheme requirement has been set.
*
* @param string $scheme
*
* @return bool true if the scheme requirement exists, otherwise false
* @return bool
*/
public function hasScheme($scheme)
public function hasScheme(string $scheme)
{
return \in_array(strtolower($scheme), $this->schemes, true);
}
@@ -227,7 +194,7 @@ class Route implements \Serializable
* Returns the uppercased HTTP methods this route is restricted to.
* So an empty array means that any method is allowed.
*
* @return string[] The methods
* @return string[]
*/
public function getMethods()
{
@@ -238,8 +205,6 @@ class Route implements \Serializable
* Sets the HTTP methods (e.g. 'POST') this route is restricted to.
* So an empty array means that any method is allowed.
*
* This method implements a fluent interface.
*
* @param string|string[] $methods The method or an array of methods
*
* @return $this
@@ -253,9 +218,7 @@ class Route implements \Serializable
}
/**
* Returns the options.
*
* @return array The options
* @return array
*/
public function getOptions()
{
@@ -263,12 +226,6 @@ class Route implements \Serializable
}
/**
* Sets the options.
*
* This method implements a fluent interface.
*
* @param array $options The options
*
* @return $this
*/
public function setOptions(array $options)
@@ -281,12 +238,6 @@ class Route implements \Serializable
}
/**
* Adds options.
*
* This method implements a fluent interface.
*
* @param array $options The options
*
* @return $this
*/
public function addOptions(array $options)
@@ -302,14 +253,11 @@ class Route implements \Serializable
/**
* Sets an option value.
*
* This method implements a fluent interface.
*
* @param string $name An option name
* @param mixed $value The option value
* @param mixed $value The option value
*
* @return $this
*/
public function setOption($name, $value)
public function setOption(string $name, $value)
{
$this->options[$name] = $value;
$this->compiled = null;
@@ -318,33 +266,25 @@ class Route implements \Serializable
}
/**
* Get an option value.
* Returns the option value or null when not found.
*
* @param string $name An option name
*
* @return mixed The option value or null when not given
* @return mixed
*/
public function getOption($name)
public function getOption(string $name)
{
return isset($this->options[$name]) ? $this->options[$name] : null;
return $this->options[$name] ?? null;
}
/**
* Checks if an option has been set.
*
* @param string $name An option name
*
* @return bool true if the option is set, false otherwise
* @return bool
*/
public function hasOption($name)
public function hasOption(string $name)
{
return \array_key_exists($name, $this->options);
}
/**
* Returns the defaults.
*
* @return array The defaults
* @return array
*/
public function getDefaults()
{
@@ -352,12 +292,6 @@ class Route implements \Serializable
}
/**
* Sets the defaults.
*
* This method implements a fluent interface.
*
* @param array $defaults The defaults
*
* @return $this
*/
public function setDefaults(array $defaults)
@@ -368,16 +302,14 @@ class Route implements \Serializable
}
/**
* Adds defaults.
*
* This method implements a fluent interface.
*
* @param array $defaults The defaults
*
* @return $this
*/
public function addDefaults(array $defaults)
{
if (isset($defaults['_locale']) && $this->isLocalized()) {
unset($defaults['_locale']);
}
foreach ($defaults as $name => $default) {
$this->defaults[$name] = $default;
}
@@ -387,25 +319,17 @@ class Route implements \Serializable
}
/**
* Gets a default value.
*
* @param string $name A variable name
*
* @return mixed The default value or null when not given
* @return mixed
*/
public function getDefault($name)
public function getDefault(string $name)
{
return isset($this->defaults[$name]) ? $this->defaults[$name] : null;
return $this->defaults[$name] ?? null;
}
/**
* Checks if a default value is set for the given variable.
*
* @param string $name A variable name
*
* @return bool true if the default value is set, false otherwise
* @return bool
*/
public function hasDefault($name)
public function hasDefault(string $name)
{
return \array_key_exists($name, $this->defaults);
}
@@ -413,13 +337,16 @@ class Route implements \Serializable
/**
* Sets a default value.
*
* @param string $name A variable name
* @param mixed $default The default value
* @param mixed $default The default value
*
* @return $this
*/
public function setDefault($name, $default)
public function setDefault(string $name, $default)
{
if ('_locale' === $name && $this->isLocalized()) {
return $this;
}
$this->defaults[$name] = $default;
$this->compiled = null;
@@ -427,9 +354,7 @@ class Route implements \Serializable
}
/**
* Returns the requirements.
*
* @return array The requirements
* @return array
*/
public function getRequirements()
{
@@ -437,12 +362,6 @@ class Route implements \Serializable
}
/**
* Sets the requirements.
*
* This method implements a fluent interface.
*
* @param array $requirements The requirements
*
* @return $this
*/
public function setRequirements(array $requirements)
@@ -453,16 +372,14 @@ class Route implements \Serializable
}
/**
* Adds requirements.
*
* This method implements a fluent interface.
*
* @param array $requirements The requirements
*
* @return $this
*/
public function addRequirements(array $requirements)
{
if (isset($requirements['_locale']) && $this->isLocalized()) {
unset($requirements['_locale']);
}
foreach ($requirements as $key => $regex) {
$this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
}
@@ -472,39 +389,30 @@ class Route implements \Serializable
}
/**
* Returns the requirement for the given key.
*
* @param string $key The key
*
* @return string|null The regex or null when not given
* @return string|null
*/
public function getRequirement($key)
public function getRequirement(string $key)
{
return isset($this->requirements[$key]) ? $this->requirements[$key] : null;
return $this->requirements[$key] ?? null;
}
/**
* Checks if a requirement is set for the given key.
*
* @param string $key A variable name
*
* @return bool true if a requirement is specified, false otherwise
* @return bool
*/
public function hasRequirement($key)
public function hasRequirement(string $key)
{
return \array_key_exists($key, $this->requirements);
}
/**
* Sets a requirement for the given key.
*
* @param string $key The key
* @param string $regex The regex
*
* @return $this
*/
public function setRequirement($key, $regex)
public function setRequirement(string $key, string $regex)
{
if ('_locale' === $key && $this->isLocalized()) {
return $this;
}
$this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
$this->compiled = null;
@@ -512,9 +420,7 @@ class Route implements \Serializable
}
/**
* Returns the condition.
*
* @return string The condition
* @return string
*/
public function getCondition()
{
@@ -522,15 +428,9 @@ class Route implements \Serializable
}
/**
* Sets the condition.
*
* This method implements a fluent interface.
*
* @param string $condition The condition
*
* @return $this
*/
public function setCondition($condition)
public function setCondition(?string $condition)
{
$this->condition = (string) $condition;
$this->compiled = null;
@@ -541,7 +441,7 @@ class Route implements \Serializable
/**
* Compiles the route.
*
* @return CompiledRoute A CompiledRoute instance
* @return CompiledRoute
*
* @throws \LogicException If the Route cannot be compiled because the
* path or host pattern is invalid
@@ -559,18 +459,38 @@ class Route implements \Serializable
return $this->compiled = $class::compile($this);
}
private function sanitizeRequirement($key, $regex)
private function extractInlineDefaultsAndRequirements(string $pattern): string
{
if (!\is_string($regex)) {
throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" must be a string.', $key));
if (false === strpbrk($pattern, '?<')) {
return $pattern;
}
if ('' !== $regex && '^' === $regex[0]) {
$regex = (string) substr($regex, 1); // returns false for a single character
return preg_replace_callback('#\{(!?)(\w++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) {
if (isset($m[4][0])) {
$this->setDefault($m[2], '?' !== $m[4] ? substr($m[4], 1) : null);
}
if (isset($m[3][0])) {
$this->setRequirement($m[2], substr($m[3], 1, -1));
}
return '{'.$m[1].$m[2].'}';
}, $pattern);
}
private function sanitizeRequirement(string $key, string $regex)
{
if ('' !== $regex) {
if ('^' === $regex[0]) {
$regex = substr($regex, 1);
} elseif (0 === strpos($regex, '\\A')) {
$regex = substr($regex, 2);
}
}
if ('$' === substr($regex, -1)) {
if (str_ends_with($regex, '$')) {
$regex = substr($regex, 0, -1);
} elseif (\strlen($regex) - 2 === strpos($regex, '\\z')) {
$regex = substr($regex, 0, -2);
}
if ('' === $regex) {
@@ -579,4 +499,9 @@ class Route implements \Serializable
return $regex;
}
private function isLocalized(): bool
{
return isset($this->defaults['_locale']) && isset($this->defaults['_canonical_route']) && ($this->requirements['_locale'] ?? null) === preg_quote($this->defaults['_locale']);
}
}

View File

@@ -12,6 +12,8 @@
namespace Symfony\Component\Routing;
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\Routing\Exception\InvalidArgumentException;
use Symfony\Component\Routing\Exception\RouteCircularReferenceException;
/**
* A RouteCollection represents a set of Route instances.
@@ -22,24 +24,40 @@ use Symfony\Component\Config\Resource\ResourceInterface;
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Tobias Schultze <http://tobion.de>
*
* @implements \IteratorAggregate<string, Route>
*/
class RouteCollection implements \IteratorAggregate, \Countable
{
/**
* @var Route[]
* @var array<string, Route>
*/
private $routes = [];
/**
* @var array
* @var array<string, Alias>
*/
private $aliases = [];
/**
* @var array<string, ResourceInterface>
*/
private $resources = [];
/**
* @var array<string, int>
*/
private $priorities = [];
public function __clone()
{
foreach ($this->routes as $name => $route) {
$this->routes[$name] = clone $route;
}
foreach ($this->aliases as $name => $alias) {
$this->aliases[$name] = clone $alias;
}
}
/**
@@ -49,56 +67,87 @@ class RouteCollection implements \IteratorAggregate, \Countable
*
* @see all()
*
* @return \ArrayIterator|Route[] An \ArrayIterator object for iterating over routes
* @return \ArrayIterator<string, Route>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->routes);
return new \ArrayIterator($this->all());
}
/**
* Gets the number of Routes in this collection.
*
* @return int The number of routes
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
{
return \count($this->routes);
}
/**
* Adds a route.
*
* @param string $name The route name
* @param Route $route A Route instance
* @param int $priority
*/
public function add($name, Route $route)
public function add(string $name, Route $route/*, int $priority = 0*/)
{
unset($this->routes[$name]);
if (\func_num_args() < 3 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) {
trigger_deprecation('symfony/routing', '5.1', 'The "%s()" method will have a new "int $priority = 0" argument in version 6.0, not defining it is deprecated.', __METHOD__);
}
unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]);
$this->routes[$name] = $route;
if ($priority = 3 <= \func_num_args() ? func_get_arg(2) : 0) {
$this->priorities[$name] = $priority;
}
}
/**
* Returns all routes in this collection.
*
* @return Route[] An array of routes
* @return array<string, Route>
*/
public function all()
{
if ($this->priorities) {
$priorities = $this->priorities;
$keysOrder = array_flip(array_keys($this->routes));
uksort($this->routes, static function ($n1, $n2) use ($priorities, $keysOrder) {
return (($priorities[$n2] ?? 0) <=> ($priorities[$n1] ?? 0)) ?: ($keysOrder[$n1] <=> $keysOrder[$n2]);
});
}
return $this->routes;
}
/**
* Gets a route by name.
*
* @param string $name The route name
*
* @return Route|null A Route instance or null when not found
* @return Route|null
*/
public function get($name)
public function get(string $name)
{
return isset($this->routes[$name]) ? $this->routes[$name] : null;
$visited = [];
while (null !== $alias = $this->aliases[$name] ?? null) {
if (false !== $searchKey = array_search($name, $visited)) {
$visited[] = $name;
throw new RouteCircularReferenceException($name, \array_slice($visited, $searchKey));
}
if ($alias->isDeprecated()) {
$deprecation = $alias->getDeprecation($name);
trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
}
$visited[] = $name;
$name = $alias->getId();
}
return $this->routes[$name] ?? null;
}
/**
@@ -109,7 +158,7 @@ class RouteCollection implements \IteratorAggregate, \Countable
public function remove($name)
{
foreach ((array) $name as $n) {
unset($this->routes[$n]);
unset($this->routes[$n], $this->priorities[$n], $this->aliases[$n]);
}
}
@@ -122,8 +171,18 @@ class RouteCollection implements \IteratorAggregate, \Countable
// we need to remove all routes with the same names first because just replacing them
// would not place the new route at the end of the merged array
foreach ($collection->all() as $name => $route) {
unset($this->routes[$name]);
unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]);
$this->routes[$name] = $route;
if (isset($collection->priorities[$name])) {
$this->priorities[$name] = $collection->priorities[$name];
}
}
foreach ($collection->getAliases() as $name => $alias) {
unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]);
$this->aliases[$name] = $alias;
}
foreach ($collection->getResources() as $resource) {
@@ -133,12 +192,8 @@ class RouteCollection implements \IteratorAggregate, \Countable
/**
* Adds a prefix to the path of all child routes.
*
* @param string $prefix An optional prefix to add before each pattern of the route collection
* @param array $defaults An array of default values
* @param array $requirements An array of requirements
*/
public function addPrefix($prefix, array $defaults = [], array $requirements = [])
public function addPrefix(string $prefix, array $defaults = [], array $requirements = [])
{
$prefix = trim(trim($prefix), '/');
@@ -159,25 +214,32 @@ class RouteCollection implements \IteratorAggregate, \Countable
public function addNamePrefix(string $prefix)
{
$prefixedRoutes = [];
$prefixedPriorities = [];
$prefixedAliases = [];
foreach ($this->routes as $name => $route) {
$prefixedRoutes[$prefix.$name] = $route;
if (null !== $name = $route->getDefault('_canonical_route')) {
$route->setDefault('_canonical_route', $prefix.$name);
if (null !== $canonicalName = $route->getDefault('_canonical_route')) {
$route->setDefault('_canonical_route', $prefix.$canonicalName);
}
if (isset($this->priorities[$name])) {
$prefixedPriorities[$prefix.$name] = $this->priorities[$name];
}
}
foreach ($this->aliases as $name => $alias) {
$prefixedAliases[$prefix.$name] = $alias->withId($prefix.$alias->getId());
}
$this->routes = $prefixedRoutes;
$this->priorities = $prefixedPriorities;
$this->aliases = $prefixedAliases;
}
/**
* Sets the host pattern on all routes.
*
* @param string $pattern The pattern
* @param array $defaults An array of default values
* @param array $requirements An array of requirements
*/
public function setHost($pattern, array $defaults = [], array $requirements = [])
public function setHost(?string $pattern, array $defaults = [], array $requirements = [])
{
foreach ($this->routes as $route) {
$route->setHost($pattern);
@@ -190,10 +252,8 @@ class RouteCollection implements \IteratorAggregate, \Countable
* Sets a condition on all routes.
*
* Existing conditions will be overridden.
*
* @param string $condition The condition
*/
public function setCondition($condition)
public function setCondition(?string $condition)
{
foreach ($this->routes as $route) {
$route->setCondition($condition);
@@ -204,8 +264,6 @@ class RouteCollection implements \IteratorAggregate, \Countable
* Adds defaults to all routes.
*
* An existing default value under the same name in a route will be overridden.
*
* @param array $defaults An array of default values
*/
public function addDefaults(array $defaults)
{
@@ -220,8 +278,6 @@ class RouteCollection implements \IteratorAggregate, \Countable
* Adds requirements to all routes.
*
* An existing requirement under the same name in a route will be overridden.
*
* @param array $requirements An array of requirements
*/
public function addRequirements(array $requirements)
{
@@ -236,8 +292,6 @@ class RouteCollection implements \IteratorAggregate, \Countable
* Adds options to all routes.
*
* An existing option value under the same name in a route will be overridden.
*
* @param array $options An array of options
*/
public function addOptions(array $options)
{
@@ -275,7 +329,7 @@ class RouteCollection implements \IteratorAggregate, \Countable
/**
* Returns an array of resources loaded to build this collection.
*
* @return ResourceInterface[] An array of resources
* @return ResourceInterface[]
*/
public function getResources()
{
@@ -294,4 +348,36 @@ class RouteCollection implements \IteratorAggregate, \Countable
$this->resources[$key] = $resource;
}
}
/**
* Sets an alias for an existing route.
*
* @param string $name The alias to create
* @param string $alias The route to alias
*
* @throws InvalidArgumentException if the alias is for itself
*/
public function addAlias(string $name, string $alias): Alias
{
if ($name === $alias) {
throw new InvalidArgumentException(sprintf('Route alias "%s" can not reference itself.', $name));
}
unset($this->routes[$name], $this->priorities[$name]);
return $this->aliases[$name] = new Alias($alias);
}
/**
* @return array<string, Alias>
*/
public function getAliases(): array
{
return $this->aliases;
}
public function getAlias(string $name): ?Alias
{
return $this->aliases[$name] ?? null;
}
}

View File

@@ -14,11 +14,16 @@ namespace Symfony\Component\Routing;
use Symfony\Component\Config\Exception\LoaderLoadException;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
trigger_deprecation('symfony/routing', '5.1', 'The "%s" class is deprecated, use "%s" instead.', RouteCollectionBuilder::class, RoutingConfigurator::class);
/**
* Helps add and import routes into a RouteCollection.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*
* @deprecated since Symfony 5.1, use RoutingConfigurator instead
*/
class RouteCollectionBuilder
{
@@ -48,15 +53,13 @@ class RouteCollectionBuilder
*
* $routes->import('blog.yml', '/blog');
*
* @param mixed $resource
* @param string|null $prefix
* @param string $type
* @param mixed $resource
*
* @return self
*
* @throws LoaderLoadException
*/
public function import($resource, $prefix = '/', $type = null)
public function import($resource, string $prefix = '/', string $type = null)
{
/** @var RouteCollection[] $collections */
$collections = $this->load($resource, $type);
@@ -87,13 +90,9 @@ class RouteCollectionBuilder
/**
* Adds a route and returns it for future modification.
*
* @param string $path The route path
* @param string $controller The route's controller
* @param string|null $name The name to give this route
*
* @return Route
*/
public function add($path, $controller, $name = null)
public function add(string $path, string $controller, string $name = null)
{
$route = new Route($path);
$route->setDefault('_controller', $controller);
@@ -114,11 +113,8 @@ class RouteCollectionBuilder
/**
* Add a RouteCollectionBuilder.
*
* @param string $prefix
* @param RouteCollectionBuilder $builder
*/
public function mount($prefix, self $builder)
public function mount(string $prefix, self $builder)
{
$builder->prefix = trim(trim($prefix), '/');
$this->routes[] = $builder;
@@ -127,12 +123,9 @@ class RouteCollectionBuilder
/**
* Adds a Route object to the builder.
*
* @param Route $route
* @param string|null $name
*
* @return $this
*/
public function addRoute(Route $route, $name = null)
public function addRoute(Route $route, string $name = null)
{
if (null === $name) {
// used as a flag to know which routes will need a name later
@@ -147,11 +140,9 @@ class RouteCollectionBuilder
/**
* Sets the host on all embedded routes (unless already set).
*
* @param string $pattern
*
* @return $this
*/
public function setHost($pattern)
public function setHost(?string $pattern)
{
$this->host = $pattern;
@@ -161,11 +152,9 @@ class RouteCollectionBuilder
/**
* Sets a condition on all embedded routes (unless already set).
*
* @param string $condition
*
* @return $this
*/
public function setCondition($condition)
public function setCondition(?string $condition)
{
$this->condition = $condition;
@@ -176,12 +165,11 @@ class RouteCollectionBuilder
* Sets a default value that will be added to all embedded routes (unless that
* default value is already set).
*
* @param string $key
* @param mixed $value
* @param mixed $value
*
* @return $this
*/
public function setDefault($key, $value)
public function setDefault(string $key, $value)
{
$this->defaults[$key] = $value;
@@ -192,12 +180,11 @@ class RouteCollectionBuilder
* Sets a requirement that will be added to all embedded routes (unless that
* requirement is already set).
*
* @param string $key
* @param mixed $regex
* @param mixed $regex
*
* @return $this
*/
public function setRequirement($key, $regex)
public function setRequirement(string $key, $regex)
{
$this->requirements[$key] = $regex;
@@ -208,12 +195,11 @@ class RouteCollectionBuilder
* Sets an option that will be added to all embedded routes (unless that
* option is already set).
*
* @param string $key
* @param mixed $value
* @param mixed $value
*
* @return $this
*/
public function setOption($key, $value)
public function setOption(string $key, $value)
{
$this->options[$key] = $value;
@@ -309,7 +295,9 @@ class RouteCollectionBuilder
} else {
/* @var self $route */
$subCollection = $route->build();
$subCollection->addPrefix($this->prefix);
if (null !== $this->prefix) {
$subCollection->addPrefix($this->prefix);
}
$routeCollection->addCollection($subCollection);
}
@@ -362,11 +350,11 @@ class RouteCollectionBuilder
}
if (null === $resolver = $this->loader->getResolver()) {
throw new LoaderLoadException($resource, null, null, null, $type);
throw new LoaderLoadException($resource, null, 0, null, $type);
}
if (false === $loader = $resolver->resolve($resource, $type)) {
throw new LoaderLoadException($resource, null, null, null, $type);
throw new LoaderLoadException($resource, null, 0, null, $type);
}
$collections = $loader->load($resource, $type);

View File

@@ -19,14 +19,17 @@ namespace Symfony\Component\Routing;
*/
class RouteCompiler implements RouteCompilerInterface
{
const REGEX_DELIMITER = '#';
/**
* @deprecated since Symfony 5.1, to be removed in 6.0
*/
public const REGEX_DELIMITER = '#';
/**
* This string defines the characters that are automatically considered separators in front of
* optional placeholders (with default and no static text following). Such a single separator
* can be left out together with the optional placeholder from matching and generating URLs.
*/
const SEPARATORS = '/,;.:-_~+*=@|';
public const SEPARATORS = '/,;.:-_~+*=@|';
/**
* The maximum supported length of a PCRE subpattern name
@@ -34,7 +37,7 @@ class RouteCompiler implements RouteCompilerInterface
*
* @internal
*/
const VARIABLE_MAXIMUM_LENGTH = 32;
public const VARIABLE_MAXIMUM_LENGTH = 32;
/**
* {@inheritdoc}
@@ -61,6 +64,14 @@ class RouteCompiler implements RouteCompilerInterface
$hostRegex = $result['regex'];
}
$locale = $route->getDefault('_locale');
if (null !== $locale && null !== $route->getDefault('_canonical_route') && preg_quote($locale) === $route->getRequirement('_locale')) {
$requirements = $route->getRequirements();
unset($requirements['_locale']);
$route->setRequirements($requirements);
$route->setPath(str_replace('{_locale}', $locale, $route->getPath()));
}
$path = $route->getPath();
$result = self::compilePattern($route, $path, false);
@@ -92,7 +103,7 @@ class RouteCompiler implements RouteCompilerInterface
);
}
private static function compilePattern(Route $route, $pattern, $isHost)
private static function compilePattern(Route $route, string $pattern, bool $isHost): array
{
$tokens = [];
$variables = [];
@@ -111,7 +122,7 @@ class RouteCompiler implements RouteCompilerInterface
// Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable
// in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
preg_match_all('#\{(!)?(\w+)\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
preg_match_all('#\{(!)?(\w+)\}#', $pattern, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER);
foreach ($matches as $match) {
$important = $match[1][1] >= 0;
$varName = $match[2][0];
@@ -127,7 +138,7 @@ class RouteCompiler implements RouteCompilerInterface
} else {
$precedingChar = substr($precedingText, -1);
}
$isSeparator = '' !== $precedingChar && false !== strpos(static::SEPARATORS, $precedingChar);
$isSeparator = '' !== $precedingChar && str_contains(static::SEPARATORS, $precedingChar);
// A PCRE subpattern name must start with a non-digit. Also a PHP variable cannot start with a digit so the
// variable would not be usable as a Controller action argument.
@@ -139,12 +150,12 @@ class RouteCompiler implements RouteCompilerInterface
}
if (\strlen($varName) > self::VARIABLE_MAXIMUM_LENGTH) {
throw new \DomainException(sprintf('Variable name "%s" cannot be longer than %s characters in route pattern "%s". Please use a shorter name.', $varName, self::VARIABLE_MAXIMUM_LENGTH, $pattern));
throw new \DomainException(sprintf('Variable name "%s" cannot be longer than %d characters in route pattern "%s". Please use a shorter name.', $varName, self::VARIABLE_MAXIMUM_LENGTH, $pattern));
}
if ($isSeparator && $precedingText !== $precedingChar) {
$tokens[] = ['text', substr($precedingText, 0, -\strlen($precedingChar))];
} elseif (!$isSeparator && \strlen($precedingText) > 0) {
} elseif (!$isSeparator && '' !== $precedingText) {
$tokens[] = ['text', $precedingText];
}
@@ -161,8 +172,8 @@ class RouteCompiler implements RouteCompilerInterface
$nextSeparator = self::findNextSeparator($followingPattern, $useUtf8);
$regexp = sprintf(
'[^%s%s]+',
preg_quote($defaultSeparator, self::REGEX_DELIMITER),
$defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator, self::REGEX_DELIMITER) : ''
preg_quote($defaultSeparator),
$defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator) : ''
);
if (('' !== $nextSeparator && !preg_match('#^\{\w+\}#', $followingPattern)) || '' === $followingPattern) {
// When we have a separator, which is disallowed for the variable, we can optimize the regex with a possessive
@@ -199,7 +210,7 @@ class RouteCompiler implements RouteCompilerInterface
}
// find the first optional token
$firstOptional = PHP_INT_MAX;
$firstOptional = \PHP_INT_MAX;
if (!$isHost) {
for ($i = \count($tokens) - 1; $i >= 0; --$i) {
$token = $tokens[$i];
@@ -217,7 +228,7 @@ class RouteCompiler implements RouteCompilerInterface
for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) {
$regexp .= self::computeRegexp($tokens, $i, $firstOptional);
}
$regexp = self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'sD'.($isHost ? 'i' : '');
$regexp = '{^'.$regexp.'$}sD'.($isHost ? 'i' : '');
// enable Utf8 matching if really required
if ($needsUtf8) {
@@ -272,7 +283,7 @@ class RouteCompiler implements RouteCompilerInterface
preg_match('/^./u', $pattern, $pattern);
}
return false !== strpos(static::SEPARATORS, $pattern[0]) ? $pattern[0] : '';
return str_contains(static::SEPARATORS, $pattern[0]) ? $pattern[0] : '';
}
/**
@@ -281,22 +292,20 @@ class RouteCompiler implements RouteCompilerInterface
* @param array $tokens The route tokens
* @param int $index The index of the current token
* @param int $firstOptional The index of the first optional token
*
* @return string The regexp pattern for a single token
*/
private static function computeRegexp(array $tokens, int $index, int $firstOptional): string
{
$token = $tokens[$index];
if ('text' === $token[0]) {
// Text tokens
return preg_quote($token[1], self::REGEX_DELIMITER);
return preg_quote($token[1]);
} else {
// Variable tokens
if (0 === $index && 0 === $firstOptional) {
// When the only token is an optional variable token, the separator is required
return sprintf('%s(?P<%s>%s)?', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
return sprintf('%s(?P<%s>%s)?', preg_quote($token[1]), $token[3], $token[2]);
} else {
$regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
$regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1]), $token[3], $token[2]);
if ($index >= $firstOptional) {
// Enclose each optional token in a subpattern to make it optional.
// "?:" means it is non-capturing, i.e. the portion of the subject string that

View File

@@ -21,7 +21,7 @@ interface RouteCompilerInterface
/**
* Compiles the current route instance.
*
* @return CompiledRoute A CompiledRoute instance
* @return CompiledRoute
*
* @throws \LogicException If the Route cannot be compiled because the
* path or host pattern is invalid

View File

@@ -12,7 +12,6 @@
namespace Symfony\Component\Routing;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher;
use Symfony\Component\Config\ConfigCacheFactory;
use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\Config\ConfigCacheInterface;
@@ -23,13 +22,11 @@ use Symfony\Component\Routing\Generator\CompiledUrlGenerator;
use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface;
use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper;
use Symfony\Component\Routing\Generator\Dumper\GeneratorDumperInterface;
use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Matcher\CompiledUrlMatcher;
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
/**
@@ -95,19 +92,17 @@ class Router implements RouterInterface, RequestMatcherInterface
*/
private $expressionLanguageProviders = [];
private static $cache = [];
/**
* @param LoaderInterface $loader A LoaderInterface instance
* @param mixed $resource The main resource to load
* @param array $options An array of options
* @param RequestContext $context The context
* @param LoggerInterface $logger A logger instance
* @param mixed $resource The main resource to load
*/
public function __construct(LoaderInterface $loader, $resource, array $options = [], RequestContext $context = null, LoggerInterface $logger = null, string $defaultLocale = null)
{
$this->loader = $loader;
$this->resource = $resource;
$this->logger = $logger;
$this->context = $context ?: new RequestContext();
$this->context = $context ?? new RequestContext();
$this->setOptions($options);
$this->defaultLocale = $defaultLocale;
}
@@ -127,8 +122,6 @@ class Router implements RouterInterface, RequestMatcherInterface
* * strict_requirements: Configure strict requirement checking for generators
* implementing ConfigurableRequirementsInterface (default is true)
*
* @param array $options An array of options
*
* @throws \InvalidArgumentException When unsupported option is provided
*/
public function setOptions(array $options)
@@ -137,13 +130,9 @@ class Router implements RouterInterface, RequestMatcherInterface
'cache_dir' => null,
'debug' => false,
'generator_class' => CompiledUrlGenerator::class,
'generator_base_class' => UrlGenerator::class, // deprecated
'generator_dumper_class' => CompiledUrlGeneratorDumper::class,
'generator_cache_class' => 'UrlGenerator', // deprecated
'matcher_class' => CompiledUrlMatcher::class,
'matcher_base_class' => UrlMatcher::class, // deprecated
'matcher_dumper_class' => CompiledUrlMatcherDumper::class,
'matcher_cache_class' => 'UrlMatcher', // deprecated
'resource_type' => null,
'strict_requirements' => true,
];
@@ -151,7 +140,6 @@ class Router implements RouterInterface, RequestMatcherInterface
// check option names and live merge, if errors are encountered Exception will be thrown
$invalid = [];
foreach ($options as $key => $value) {
$this->checkDeprecatedOption($key);
if (\array_key_exists($key, $this->options)) {
$this->options[$key] = $value;
} else {
@@ -167,39 +155,32 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* Sets an option.
*
* @param string $key The key
* @param mixed $value The value
* @param mixed $value The value
*
* @throws \InvalidArgumentException
*/
public function setOption($key, $value)
public function setOption(string $key, $value)
{
if (!\array_key_exists($key, $this->options)) {
throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key));
}
$this->checkDeprecatedOption($key);
$this->options[$key] = $value;
}
/**
* Gets an option value.
*
* @param string $key The key
*
* @return mixed The value
* @return mixed
*
* @throws \InvalidArgumentException
*/
public function getOption($key)
public function getOption(string $key)
{
if (!\array_key_exists($key, $this->options)) {
throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key));
}
$this->checkDeprecatedOption($key);
return $this->options[$key];
}
@@ -249,7 +230,7 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* {@inheritdoc}
*/
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH)
{
return $this->getGenerator()->generate($name, $parameters, $referenceType);
}
@@ -257,7 +238,7 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* {@inheritdoc}
*/
public function match($pathinfo)
public function match(string $pathinfo)
{
return $this->getMatcher()->match($pathinfo);
}
@@ -277,9 +258,9 @@ class Router implements RouterInterface, RequestMatcherInterface
}
/**
* Gets the UrlMatcher instance associated with this Router.
* Gets the UrlMatcher or RequestMatcher instance associated with this Router.
*
* @return UrlMatcherInterface A UrlMatcherInterface instance
* @return UrlMatcherInterface|RequestMatcherInterface
*/
public function getMatcher()
{
@@ -287,10 +268,9 @@ class Router implements RouterInterface, RequestMatcherInterface
return $this->matcher;
}
$compiled = is_a($this->options['matcher_class'], CompiledUrlMatcher::class, true) && (UrlMatcher::class === $this->options['matcher_base_class'] || RedirectableUrlMatcher::class === $this->options['matcher_base_class']);
if (null === $this->options['cache_dir'] || null === $this->options['matcher_cache_class']) {
if (null === $this->options['cache_dir']) {
$routes = $this->getRouteCollection();
$compiled = is_a($this->options['matcher_class'], CompiledUrlMatcher::class, true);
if ($compiled) {
$routes = (new CompiledUrlMatcherDumper($routes))->getCompiledRoutes();
}
@@ -304,7 +284,7 @@ class Router implements RouterInterface, RequestMatcherInterface
return $this->matcher;
}
$cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/'.$this->options['matcher_cache_class'].'.php',
$cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/url_matching_routes.php',
function (ConfigCacheInterface $cache) {
$dumper = $this->getMatcherDumperInstance();
if (method_exists($dumper, 'addExpressionLanguageProvider')) {
@@ -313,30 +293,17 @@ class Router implements RouterInterface, RequestMatcherInterface
}
}
$options = [
'class' => $this->options['matcher_cache_class'],
'base_class' => $this->options['matcher_base_class'],
];
$cache->write($dumper->dump($options), $this->getRouteCollection()->getResources());
$cache->write($dumper->dump(), $this->getRouteCollection()->getResources());
}
);
if ($compiled) {
return $this->matcher = new $this->options['matcher_class'](require $cache->getPath(), $this->context);
}
if (!class_exists($this->options['matcher_cache_class'], false)) {
require_once $cache->getPath();
}
return $this->matcher = new $this->options['matcher_cache_class']($this->context);
return $this->matcher = new $this->options['matcher_class'](self::getCompiledRoutes($cache->getPath()), $this->context);
}
/**
* Gets the UrlGenerator instance associated with this Router.
*
* @return UrlGeneratorInterface A UrlGeneratorInterface instance
* @return UrlGeneratorInterface
*/
public function getGenerator()
{
@@ -344,37 +311,26 @@ class Router implements RouterInterface, RequestMatcherInterface
return $this->generator;
}
$compiled = is_a($this->options['generator_class'], CompiledUrlGenerator::class, true) && UrlGenerator::class === $this->options['generator_base_class'];
if (null === $this->options['cache_dir'] || null === $this->options['generator_cache_class']) {
if (null === $this->options['cache_dir']) {
$routes = $this->getRouteCollection();
$aliases = [];
$compiled = is_a($this->options['generator_class'], CompiledUrlGenerator::class, true);
if ($compiled) {
$routes = (new CompiledUrlGeneratorDumper($routes))->getCompiledRoutes();
$generatorDumper = new CompiledUrlGeneratorDumper($routes);
$routes = $generatorDumper->getCompiledRoutes();
$aliases = $generatorDumper->getCompiledAliases();
}
$this->generator = new $this->options['generator_class']($routes, $this->context, $this->logger, $this->defaultLocale);
$this->generator = new $this->options['generator_class'](array_merge($routes, $aliases), $this->context, $this->logger, $this->defaultLocale);
} else {
$cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/'.$this->options['generator_cache_class'].'.php',
$cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/url_generating_routes.php',
function (ConfigCacheInterface $cache) {
$dumper = $this->getGeneratorDumperInstance();
$options = [
'class' => $this->options['generator_cache_class'],
'base_class' => $this->options['generator_base_class'],
];
$cache->write($dumper->dump($options), $this->getRouteCollection()->getResources());
$cache->write($dumper->dump(), $this->getRouteCollection()->getResources());
}
);
if ($compiled) {
$this->generator = new $this->options['generator_class'](require $cache->getPath(), $this->context, $this->logger);
} else {
if (!class_exists($this->options['generator_cache_class'], false)) {
require_once $cache->getPath();
}
$this->generator = new $this->options['generator_cache_class']($this->context, $this->logger, $this->defaultLocale);
}
$this->generator = new $this->options['generator_class'](self::getCompiledRoutes($cache->getPath()), $this->context, $this->logger, $this->defaultLocale);
}
if ($this->generator instanceof ConfigurableRequirementsInterface) {
@@ -408,10 +364,8 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* Provides the ConfigCache factory implementation, falling back to a
* default implementation if necessary.
*
* @return ConfigCacheFactoryInterface
*/
private function getConfigCacheFactory()
private function getConfigCacheFactory(): ConfigCacheFactoryInterface
{
if (null === $this->configCacheFactory) {
$this->configCacheFactory = new ConfigCacheFactory($this->options['debug']);
@@ -420,14 +374,20 @@ class Router implements RouterInterface, RequestMatcherInterface
return $this->configCacheFactory;
}
private function checkDeprecatedOption($key)
private static function getCompiledRoutes(string $path): array
{
switch ($key) {
case 'generator_base_class':
case 'generator_cache_class':
case 'matcher_base_class':
case 'matcher_cache_class':
@trigger_error(sprintf('Option "%s" given to router %s is deprecated since Symfony 4.3.', $key, static::class), E_USER_DEPRECATED);
if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN))) {
self::$cache = null;
}
if (null === self::$cache) {
return require $path;
}
if (isset(self::$cache[$path])) {
return self::$cache[$path];
}
return self::$cache[$path] = require $path;
}
}

View File

@@ -26,7 +26,10 @@ interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface
/**
* Gets the RouteCollection instance associated with this Router.
*
* @return RouteCollection A RouteCollection instance
* WARNING: This method should never be used at runtime as it is SLOW.
* You might use it in a cache warmer though.
*
* @return RouteCollection
*/
public function getRouteCollection();
}

View File

@@ -1,59 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Annotation;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\Annotation\Route;
class RouteTest extends TestCase
{
/**
* @expectedException \BadMethodCallException
*/
public function testInvalidRouteParameter()
{
$route = new Route(['foo' => 'bar']);
}
/**
* @expectedException \BadMethodCallException
*/
public function testTryingToSetLocalesDirectly()
{
$route = new Route(['locales' => ['nl' => 'bar']]);
}
/**
* @dataProvider getValidParameters
*/
public function testRouteParameters($parameter, $value, $getter)
{
$route = new Route([$parameter => $value]);
$this->assertEquals($route->$getter(), $value);
}
public function getValidParameters()
{
return [
['value', '/Blog', 'getPath'],
['requirements', ['locale' => 'en'], 'getRequirements'],
['options', ['compiler_class' => 'RouteCompiler'], 'getOptions'],
['name', 'blog_index', 'getName'],
['defaults', ['_controller' => 'MyBlogBundle:Blog:index'], 'getDefaults'],
['schemes', ['https'], 'getSchemes'],
['methods', ['GET', 'POST'], 'getMethods'],
['host', '{locale}.example.com', 'getHost'],
['condition', 'context.getMethod() == "GET"', 'getCondition'],
['value', ['nl' => '/hier', 'en' => '/here'], 'getLocalizedPaths'],
];
}
}

View File

@@ -1,27 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\CompiledRoute;
class CompiledRouteTest extends TestCase
{
public function testAccessors()
{
$compiled = new CompiledRoute('prefix', 'regex', ['tokens'], [], null, [], [], ['variables']);
$this->assertEquals('prefix', $compiled->getStaticPrefix(), '__construct() takes a static prefix as its second argument');
$this->assertEquals('regex', $compiled->getRegex(), '__construct() takes a regexp as its third argument');
$this->assertEquals(['tokens'], $compiled->getTokens(), '__construct() takes an array of tokens as its fourth argument');
$this->assertEquals(['variables'], $compiled->getVariables(), '__construct() takes an array of variables as its ninth argument');
}
}

View File

@@ -1,36 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
class RoutingResolverPassTest extends TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$container->register('routing.resolver', LoaderResolver::class);
$container->register('loader1')->addTag('routing.loader');
$container->register('loader2')->addTag('routing.loader');
(new RoutingResolverPass())->process($container);
$this->assertEquals(
[['addLoader', [new Reference('loader1')]], ['addLoader', [new Reference('loader2')]]],
$container->getDefinition('routing.resolver')->getMethodCalls()
);
}
}

View File

@@ -1,21 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses;
abstract class AbstractClass
{
abstract public function abstractRouteAction();
public function routeAction($arg1, $arg2 = 'defaultValue2', $arg3 = 'defaultValue3')
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses;
class BarClass
{
public function routeAction($arg1, $arg2 = 'defaultValue2', $arg3 = 'defaultValue3')
{
}
}

View File

@@ -1,10 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses;
class EncodingClass
{
public function routeÀction()
{
}
}

View File

@@ -1,13 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses;
trait FooTrait
{
public function doBar()
{
$baz = self::class;
if (true) {
}
}
}

View File

@@ -1,7 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
abstract class AbstractClassController
{
}

View File

@@ -1,15 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
class ActionPathController
{
/**
* @Route("/path", name="action")
*/
public function action()
{
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
class DefaultValueController
{
/**
* @Route("/{default}/path", name="action")
*/
public function action($default = 'value')
{
}
/**
* @Route("/hello/{name<\w+>}", name="hello_without_default")
* @Route("/hello/{name<\w+>?Symfony}", name="hello_with_default")
*/
public function hello(string $name = 'World')
{
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
class ExplicitLocalizedActionPathController
{
/**
* @Route(path={"en": "/path", "nl": "/pad"}, name="action")
*/
public function action()
{
}
}

View File

@@ -1,34 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/defaults", locale="g_locale", format="g_format")
*/
class GlobalDefaultsClass
{
/**
* @Route("/specific-locale", name="specific_locale", locale="s_locale")
*/
public function locale()
{
}
/**
* @Route("/specific-format", name="specific_format", format="s_format")
*/
public function format()
{
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/here", name="lol", methods={"GET", "POST"}, schemes={"https"})
*/
class InvokableController
{
public function __invoke()
{
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route(path={"nl": "/hier", "en": "/here"}, name="action")
*/
class InvokableLocalizedController
{
public function __invoke()
{
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
class LocalizedActionPathController
{
/**
* @Route(path={"en": "/path", "nl": "/pad"}, name="action")
*/
public function action()
{
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route(path={"en": "/the/path", "nl": "/het/pad"})
*/
class LocalizedMethodActionControllers
{
/**
* @Route(name="post", methods={"POST"})
*/
public function post()
{
}
/**
* @Route(name="put", methods={"PUT"})
*/
public function put()
{
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route(path={"nl": "/nl", "en": "/en"})
*/
class LocalizedPrefixLocalizedActionController
{
/**
* @Route(path={"nl": "/actie", "en": "/action"}, name="action")
*/
public function action()
{
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route(path={"nl": "/nl"})
*/
class LocalizedPrefixMissingLocaleActionController
{
/**
* @Route(path={"nl": "/actie", "en": "/action"}, name="action")
*/
public function action()
{
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route(path={"nl": "/nl", "en": "/en"})
*/
class LocalizedPrefixMissingRouteLocaleActionController
{
/**
* @Route(path={"nl": "/actie"}, name="action")
*/
public function action()
{
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route(path={"en": "/en", "nl": "/nl"})
*/
class LocalizedPrefixWithRouteWithoutLocale
{
/**
* @Route("/suffix", name="action")
*/
public function action()
{
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/the/path")
*/
class MethodActionControllers
{
/**
* @Route(name="post", methods={"POST"})
*/
public function post()
{
}
/**
* @Route(name="put", methods={"PUT"})
*/
public function put()
{
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
class MissingRouteNameController
{
/**
* @Route("/path")
*/
public function action()
{
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
class NothingButNameController
{
/**
* @Route(name="action")
*/
public function action()
{
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/prefix")
*/
class PrefixedActionLocalizedRouteController
{
/**
* @Route(path={"en": "/path", "nl": "/pad"}, name="action")
*/
public function action()
{
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/prefix", host="frankdejonge.nl", condition="lol=fun")
*/
class PrefixedActionPathController
{
/**
* @Route("/path", name="action")
*/
public function action()
{
}
}

View File

@@ -1,27 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/", requirements={"foo", "\d+"})
*/
class RequirementsWithoutPlaceholderNameController
{
/**
* @Route("/{foo}", name="foo", requirements={"foo", "\d+"})
*/
public function foo()
{
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/prefix")
*/
class RouteWithPrefixController
{
/**
* @Route("/path", name="action")
*/
public function action()
{
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/test", utf8=true)
*/
class Utf8ActionControllers
{
/**
* @Route(name="one")
*/
public function one()
{
}
/**
* @Route(name="two", utf8=false)
*/
public function two()
{
}
}

View File

@@ -1,18 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures;
use Symfony\Component\Routing\CompiledRoute;
class CustomCompiledRoute extends CompiledRoute
{
}

View File

@@ -1,26 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCompiler;
class CustomRouteCompiler extends RouteCompiler
{
/**
* {@inheritdoc}
*/
public static function compile(Route $route)
{
return new CustomCompiledRoute('', '', [], []);
}
}

View File

@@ -1,26 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Routing\Loader\XmlFileLoader;
/**
* XmlFileLoader with schema validation turned off.
*/
class CustomXmlFileLoader extends XmlFileLoader
{
protected function loadFile($file)
{
return XmlUtils::loadFile($file, function () { return true; });
}
}

View File

@@ -1,24 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\OtherAnnotatedClasses;
trait AnonymousClassInTrait
{
public function test()
{
return new class() {
public function foo()
{
}
};
}
}

View File

@@ -1,3 +0,0 @@
class NoStartTagClass
{
}

View File

@@ -1,19 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures\OtherAnnotatedClasses;
class VariadicClass
{
public function routeAction(...$params)
{
}
}

View File

@@ -1,30 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures;
use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
use Symfony\Component\Routing\Matcher\UrlMatcher;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface
{
public function redirect($path, $route, $scheme = null)
{
return [
'_controller' => 'Some controller reference...',
'path' => $path,
'scheme' => $scheme,
];
}
}

Some files were not shown because too many files have changed in this diff Show More