Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

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

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

@@ -0,0 +1,93 @@
<?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 string $id;
private array $deprecation = [];
public function __construct(string $id)
{
$this->id = $id;
}
public function withId(string $id): static
{
$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): static
{
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,46 +15,68 @@ 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;
private $name;
private $requirements = array();
private $options = array();
private $defaults = array();
private $host;
private $methods = array();
private $schemes = array();
private $condition;
private ?string $path = null;
private array $localizedPaths = [];
private array $methods;
private array $schemes;
/**
* Constructor.
*
* @param array $data An array of key/value parameters
*
* @throws \BadMethodCallException
* @param string[] $requirements
* @param string[]|string $methods
* @param string[]|string $schemes
*/
public function __construct(array $data)
{
if (isset($data['value'])) {
$data['path'] = $data['value'];
unset($data['value']);
public function __construct(
string|array $path = null,
private ?string $name = null,
private array $requirements = [],
private array $options = [],
private array $defaults = [],
private ?string $host = null,
array|string $methods = [],
array|string $schemes = [],
private ?string $condition = null,
private ?int $priority = null,
string $locale = null,
string $format = null,
bool $utf8 = null,
bool $stateless = null,
private ?string $env = null
) {
if (\is_array($path)) {
$this->localizedPaths = $path;
} else {
$this->path = $path;
}
$this->setMethods($methods);
$this->setSchemes($schemes);
if (null !== $locale) {
$this->defaults['_locale'] = $locale;
}
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)));
}
$this->$method($value);
if (null !== $format) {
$this->defaults['_format'] = $format;
}
if (null !== $utf8) {
$this->options['utf8'] = $utf8;
}
if (null !== $stateless) {
$this->defaults['_stateless'] = $stateless;
}
}
public function setPath($path)
public function setPath(string $path)
{
$this->path = $path;
}
@@ -64,7 +86,17 @@ class Route
return $this->path;
}
public function setHost($pattern)
public function setLocalizedPaths(array $localizedPaths)
{
$this->localizedPaths = $localizedPaths;
}
public function getLocalizedPaths(): array
{
return $this->localizedPaths;
}
public function setHost(string $pattern)
{
$this->host = $pattern;
}
@@ -74,7 +106,7 @@ class Route
return $this->host;
}
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
@@ -84,7 +116,7 @@ class Route
return $this->name;
}
public function setRequirements($requirements)
public function setRequirements(array $requirements)
{
$this->requirements = $requirements;
}
@@ -94,7 +126,7 @@ class Route
return $this->requirements;
}
public function setOptions($options)
public function setOptions(array $options)
{
$this->options = $options;
}
@@ -104,7 +136,7 @@ class Route
return $this->options;
}
public function setDefaults($defaults)
public function setDefaults(array $defaults)
{
$this->defaults = $defaults;
}
@@ -114,9 +146,9 @@ class Route
return $this->defaults;
}
public function setSchemes($schemes)
public function setSchemes(array|string $schemes)
{
$this->schemes = is_array($schemes) ? $schemes : array($schemes);
$this->schemes = (array) $schemes;
}
public function getSchemes()
@@ -124,9 +156,9 @@ class Route
return $this->schemes;
}
public function setMethods($methods)
public function setMethods(array|string $methods)
{
$this->methods = is_array($methods) ? $methods : array($methods);
$this->methods = (array) $methods;
}
public function getMethods()
@@ -134,7 +166,7 @@ class Route
return $this->methods;
}
public function setCondition($condition)
public function setCondition(?string $condition)
{
$this->condition = $condition;
}
@@ -143,4 +175,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,25 +1,102 @@
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
-----
* added `CompiledUrlMatcher` and `CompiledUrlMatcherDumper`
* added `CompiledUrlGenerator` and `CompiledUrlGeneratorDumper`
* deprecated `PhpGeneratorDumper` and `PhpMatcherDumper`
* deprecated `generator_base_class`, `generator_cache_class`, `matcher_base_class` and `matcher_cache_class` router options
* `Serializable` implementing methods for `Route` and `CompiledRoute` are marked as `@internal` and `@final`.
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 service route loaders
4.2.0
-----
* added fallback to cultureless locale for internationalized routes
4.0.0
-----
* dropped support for using UTF-8 route patterns without using the `utf8` option
* dropped support for using UTF-8 route requirements without using the `utf8` option
3.4.0
-----
* Added `NoConfigurationException`.
* Added the possibility to define a prefix for all routes of a controller via @Route(name="prefix_")
* Added support for prioritized routing loaders.
* Add matched and default parameters to redirect responses
* Added support for a `controller` keyword for configuring route controllers in YAML and XML configurations.
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
-----
* Added support for `bool`, `int`, `float`, `string`, `list` and `map` defaults in XML configurations.
* Added support for UTF-8 requirements
2.8.0
-----
@@ -32,7 +109,7 @@ CHANGELOG
Before:
```php
$router->generate('blog_show', array('slug' => 'my-blog-post'), true);
$router->generate('blog_show', ['slug' => 'my-blog-post'], true);
```
After:
@@ -40,7 +117,7 @@ CHANGELOG
```php
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
$router->generate('blog_show', array('slug' => 'my-blog-post'), UrlGeneratorInterface::ABSOLUTE_URL);
$router->generate('blog_show', ['slug' => 'my-blog-post'], UrlGeneratorInterface::ABSOLUTE_URL);
```
2.5.0
@@ -48,7 +125,7 @@ CHANGELOG
* [DEPRECATION] The `ApacheMatcherDumper` and `ApacheUrlMatcher` were deprecated and
will be removed in Symfony 3.0, since the performance gains were minimal and
it's hard to replicate the behaviour of PHP implementation.
it's hard to replicate the behavior of PHP implementation.
2.3.0
-----
@@ -105,7 +182,7 @@ CHANGELOG
```php
$route = new Route();
$route->setPath('/article/{id}');
$route->setMethods(array('POST', 'PUT'));
$route->setMethods(['POST', 'PUT']);
$route->setSchemes('https');
```
@@ -160,10 +237,10 @@ CHANGELOG
used with a single parameter. The other params `$prefix`, `$default`, `$requirements` and `$options`
will still work, but have been deprecated. The `addPrefix` method should be used for this
use-case instead.
Before: `$parentCollection->addCollection($collection, '/prefix', array(...), array(...))`
Before: `$parentCollection->addCollection($collection, '/prefix', [...], [...])`
After:
```php
$collection->addPrefix('/prefix', array(...), array(...));
$collection->addPrefix('/prefix', [...], [...]);
$parentCollection->addCollection($collection);
```
* added support for the method default argument values when defining a @Route
@@ -188,7 +265,7 @@ CHANGELOG
(only relevant if you implemented your own RouteCompiler).
* Added possibility to generate relative paths and network paths in the UrlGenerator, e.g.
"../parent-file" and "//example.com/dir/file". The third parameter in
`UrlGeneratorInterface::generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)`
`UrlGeneratorInterface::generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)`
now accepts more values and you should use the constants defined in `UrlGeneratorInterface` for
claritiy. The old method calls with a Boolean parameter will continue to work because they
equal the signature using the constants.

View File

@@ -18,18 +18,16 @@ namespace Symfony\Component\Routing;
*/
class CompiledRoute implements \Serializable
{
private $variables;
private $tokens;
private $staticPrefix;
private $regex;
private $pathVariables;
private $hostVariables;
private $hostRegex;
private $hostTokens;
private array $variables;
private array $tokens;
private string $staticPrefix;
private string $regex;
private array $pathVariables;
private array $hostVariables;
private ?string $hostRegex;
private array $hostTokens;
/**
* Constructor.
*
* @param string $staticPrefix The static prefix of the compiled route
* @param string $regex The regular expression to use to match this route
* @param array $tokens An array of tokens to use to generate URL for this route
@@ -39,9 +37,9 @@ class CompiledRoute implements \Serializable
* @param array $hostVariables An array of host variables
* @param array $variables An array of variables (variables defined in the path and in the host patterns)
*/
public function __construct($staticPrefix, $regex, array $tokens, array $pathVariables, $hostRegex = null, array $hostTokens = array(), array $hostVariables = array(), array $variables = array())
public function __construct(string $staticPrefix, string $regex, array $tokens, array $pathVariables, string $hostRegex = null, array $hostTokens = [], array $hostVariables = [], array $variables = [])
{
$this->staticPrefix = (string) $staticPrefix;
$this->staticPrefix = $staticPrefix;
$this->regex = $regex;
$this->tokens = $tokens;
$this->pathVariables = $pathVariables;
@@ -51,12 +49,9 @@ class CompiledRoute implements \Serializable
$this->variables = $variables;
}
/**
* {@inheritdoc}
*/
public function serialize()
public function __serialize(): array
{
return serialize(array(
return [
'vars' => $this->variables,
'path_prefix' => $this->staticPrefix,
'path_regex' => $this->regex,
@@ -65,20 +60,19 @@ class CompiledRoute implements \Serializable
'host_regex' => $this->hostRegex,
'host_tokens' => $this->hostTokens,
'host_vars' => $this->hostVariables,
));
];
}
/**
* {@inheritdoc}
* @internal
*/
public function unserialize($serialized)
final public function serialize(): string
{
if (\PHP_VERSION_ID >= 70000) {
$data = unserialize($serialized, array('allowed_classes' => false));
} else {
$data = unserialize($serialized);
}
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __unserialize(array $data): void
{
$this->variables = $data['vars'];
$this->staticPrefix = $data['path_prefix'];
$this->regex = $data['path_regex'];
@@ -90,81 +84,73 @@ class CompiledRoute implements \Serializable
}
/**
* Returns the static prefix.
*
* @return string The static prefix
* @internal
*/
public function getStaticPrefix()
final public function unserialize(string $serialized)
{
$this->__unserialize(unserialize($serialized, ['allowed_classes' => false]));
}
/**
* Returns the static prefix.
*/
public function getStaticPrefix(): string
{
return $this->staticPrefix;
}
/**
* Returns the regex.
*
* @return string The regex
*/
public function getRegex()
public function getRegex(): string
{
return $this->regex;
}
/**
* Returns the host regex.
*
* @return string|null The host regex or null
*/
public function getHostRegex()
public function getHostRegex(): ?string
{
return $this->hostRegex;
}
/**
* Returns the tokens.
*
* @return array The tokens
*/
public function getTokens()
public function getTokens(): array
{
return $this->tokens;
}
/**
* Returns the host tokens.
*
* @return array The tokens
*/
public function getHostTokens()
public function getHostTokens(): array
{
return $this->hostTokens;
}
/**
* Returns the variables.
*
* @return array The variables
*/
public function getVariables()
public function getVariables(): array
{
return $this->variables;
}
/**
* Returns the path variables.
*
* @return array The variables
*/
public function getPathVariables()
public function getPathVariables(): array
{
return $this->pathVariables;
}
/**
* Returns the host variables.
*
* @return array The variables
*/
public function getHostVariables()
public function getHostVariables(): array
{
return $this->hostVariables;
}

View File

@@ -11,9 +11,10 @@
namespace Symfony\Component\Routing\DependencyInjection;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Adds tagged routing.loader services to routing.resolver service.
@@ -22,25 +23,18 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
*/
class RoutingResolverPass implements CompilerPassInterface
{
private $resolverServiceId;
private $loaderTag;
public function __construct($resolverServiceId = 'routing.resolver', $loaderTag = 'routing.loader')
{
$this->resolverServiceId = $resolverServiceId;
$this->loaderTag = $loaderTag;
}
use PriorityTaggedServiceTrait;
public function process(ContainerBuilder $container)
{
if (false === $container->hasDefinition($this->resolverServiceId)) {
if (false === $container->hasDefinition('routing.resolver')) {
return;
}
$definition = $container->getDefinition($this->resolverServiceId);
$definition = $container->getDefinition('routing.resolver');
foreach ($container->findTaggedServiceIds($this->loaderTag, true) as $id => $attributes) {
$definition->addMethodCall('addLoader', array(new Reference($id)));
foreach ($this->findAndSortTaggedServices('routing.loader', $container) as $id) {
$definition->addMethodCall('addLoader', [new Reference($id)]);
}
}
}

View File

@@ -16,6 +16,6 @@ namespace Symfony\Component\Routing\Exception;
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
interface ExceptionInterface
interface ExceptionInterface extends \Throwable
{
}

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

@@ -20,12 +20,12 @@ namespace Symfony\Component\Routing\Exception;
*/
class MethodNotAllowedException extends \RuntimeException implements ExceptionInterface
{
/**
* @var array
*/
protected $allowedMethods = array();
protected $allowedMethods = [];
public function __construct(array $allowedMethods, $message = null, $code = 0, \Exception $previous = null)
/**
* @param string[] $allowedMethods
*/
public function __construct(array $allowedMethods, string $message = '', int $code = 0, \Throwable $previous = null)
{
$this->allowedMethods = array_map('strtoupper', $allowedMethods);
@@ -35,9 +35,9 @@ class MethodNotAllowedException extends \RuntimeException implements ExceptionIn
/**
* Gets the allowed HTTP methods.
*
* @return array
* @return string[]
*/
public function getAllowedMethods()
public function getAllowedMethods(): array
{
return $this->allowedMethods;
}

View File

@@ -9,10 +9,13 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Fixtures;
namespace Symfony\Component\Routing\Exception;
use Symfony\Component\Routing\CompiledRoute;
class CustomCompiledRoute extends CompiledRoute
/**
* Exception thrown when no routes are configured.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class NoConfigurationException extends ResourceNotFoundException
{
}

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;
abstract class AbstractClass
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,69 @@
<?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;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\RequestContext;
/**
* Generates URLs based on rules dumped by CompiledUrlGeneratorDumper.
*/
class CompiledUrlGenerator extends UrlGenerator
{
private array $compiledRoutes = [];
private ?string $defaultLocale;
public function __construct(array $compiledRoutes, RequestContext $context, LoggerInterface $logger = null, string $defaultLocale = null)
{
$this->compiledRoutes = $compiledRoutes;
$this->context = $context;
$this->logger = $logger;
$this->defaultLocale = $defaultLocale;
}
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
{
$locale = $parameters['_locale']
?? $this->context->getParameter('_locale')
?: $this->defaultLocale;
if (null !== $locale) {
do {
if (($this->compiledRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) {
$name .= '.'.$locale;
break;
}
} while (false !== $locale = strstr($locale, '_', true));
}
if (!isset($this->compiledRoutes[$name])) {
throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $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

@@ -20,7 +20,7 @@ namespace Symfony\Component\Routing\Generator;
* The possible configurations and use-cases:
* - setStrictRequirements(true): Throw an exception for mismatching requirements. This
* is mostly useful in development environment.
* - setStrictRequirements(false): Don't throw an exception but return null as URL for
* - setStrictRequirements(false): Don't throw an exception but return an empty string as URL for
* mismatching requirements and log the problem. Useful when you cannot control all
* params because they come from third party libs but don't want to have a 404 in
* production environment. It should log the mismatch so one can review it.
@@ -40,16 +40,12 @@ 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.
* Null means the requirements check is deactivated completely.
*
* @return bool|null
*/
public function isStrictRequirements();
public function isStrictRequirements(): ?bool;
}

View File

@@ -0,0 +1,124 @@
<?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;
use Symfony\Component\Routing\Exception\RouteCircularReferenceException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
/**
* CompiledUrlGeneratorDumper creates a PHP array to be used with CompiledUrlGenerator.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Tobias Schultze <http://tobion.de>
* @author Nicolas Grekas <p@tchwork.com>
*/
class CompiledUrlGeneratorDumper extends GeneratorDumper
{
public function getCompiledRoutes(): array
{
$compiledRoutes = [];
foreach ($this->getRoutes()->all() as $name => $route) {
$compiledRoute = $route->compile();
$compiledRoutes[$name] = [
$compiledRoute->getVariables(),
$route->getDefaults(),
$route->getRequirements(),
$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}
*/
public function dump(array $options = []): string
{
return <<<EOF
<?php
// This file has been auto-generated by the Symfony Routing Component.
return [{$this->generateDeclaredRoutes()}
];
EOF;
}
/**
* Generates PHP code representing an array of defined routes
* together with the routes properties (e.g. requirements).
*/
private function generateDeclaredRoutes(): string
{
$routes = '';
foreach ($this->getCompiledRoutes() as $name => $properties) {
$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

@@ -20,16 +20,8 @@ use Symfony\Component\Routing\RouteCollection;
*/
abstract class GeneratorDumper implements GeneratorDumperInterface
{
/**
* @var RouteCollection
*/
private $routes;
/**
* Constructor.
*
* @param RouteCollection $routes The RouteCollection to dump
*/
public function __construct(RouteCollection $routes)
{
$this->routes = $routes;
@@ -38,7 +30,7 @@ abstract class GeneratorDumper implements GeneratorDumperInterface
/**
* {@inheritdoc}
*/
public function getRoutes()
public function getRoutes(): RouteCollection
{
return $this->routes;
}

View File

@@ -23,17 +23,11 @@ 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
*/
public function dump(array $options = array());
public function dump(array $options = []): string;
/**
* Gets the routes to dump.
*
* @return RouteCollection A RouteCollection instance
*/
public function getRoutes();
public function getRoutes(): RouteCollection;
}

View File

@@ -1,123 +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;
/**
* 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>
*/
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 = array())
{
$options = array_merge(array(
'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;
/**
* {$options['class']}
*
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class {$options['class']} extends {$options['base_class']}
{
private static \$declaredRoutes;
/**
* Constructor.
*/
public function __construct(RequestContext \$context, LoggerInterface \$logger = null)
{
\$this->context = \$context;
\$this->logger = \$logger;
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 = "array(\n";
foreach ($this->getRoutes()->all() as $name => $route) {
$compiledRoute = $route->compile();
$properties = array();
$properties[] = $compiledRoute->getVariables();
$properties[] = $route->getDefaults();
$properties[] = $route->getRequirements();
$properties[] = $compiledRoute->getTokens();
$properties[] = $compiledRoute->getHostTokens();
$properties[] = $route->getSchemes();
$routes .= sprintf(" '%s' => %s,\n", $name, str_replace("\n", '', var_export($properties, true)));
}
$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 = array(), $referenceType = self::ABSOLUTE_PATH)
{
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

@@ -11,12 +11,12 @@
namespace Symfony\Component\Routing\Generator;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Exception\InvalidParameterException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Exception\InvalidParameterException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
/**
* UrlGenerator can generate a URL or a path for any route in the RouteCollection
@@ -27,14 +27,21 @@ use Psr\Log\LoggerInterface;
*/
class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInterface
{
/**
* @var RouteCollection
*/
protected $routes;
private const QUERY_FRAGMENT_DECODED = [
// RFC 3986 explicitly allows those in the query/fragment to reference other URIs unencoded
'%2F' => '/',
'%3F' => '?',
// reserved chars that have no special meaning for HTTP URIs in a query or fragment
// this excludes esp. "&", "=" and also "+" because PHP would treat it as a space (form-encoded)
'%40' => '@',
'%3A' => ':',
'%21' => '!',
'%3B' => ';',
'%2C' => ',',
'%2A' => '*',
];
/**
* @var RequestContext
*/
protected $routes;
protected $context;
/**
@@ -42,11 +49,10 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
*/
protected $strictRequirements = true;
/**
* @var LoggerInterface|null
*/
protected $logger;
private ?string $defaultLocale;
/**
* This array defines the characters (besides alphanumeric ones) that will not be percent-encoded in the path segment of the generated URL.
*
@@ -55,11 +61,12 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
* "?" and "#" (would be interpreted wrongly as query and fragment identifier),
* "'" and """ (are used as delimiters in HTML).
*/
protected $decodedChars = array(
protected $decodedChars = [
// the slash can be used to designate a hierarchical structure and we want allow using it with this meaning
// some webservers don't allow the slash in encoded form in the path for security reasons anyway
// see http://stackoverflow.com/questions/4069002/http-400-if-2f-part-of-get-url-in-jboss
'%2F' => '/',
'%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' => '@',
@@ -73,20 +80,14 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
'%21' => '!',
'%2A' => '*',
'%7C' => '|',
);
];
/**
* Constructor.
*
* @param RouteCollection $routes A RouteCollection instance
* @param RequestContext $context The context
* @param LoggerInterface|null $logger A logger instance
*/
public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $logger = null)
public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $logger = null, string $defaultLocale = null)
{
$this->routes = $routes;
$this->context = $context;
$this->logger = $logger;
$this->defaultLocale = $defaultLocale;
}
/**
@@ -100,7 +101,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
/**
* {@inheritdoc}
*/
public function getContext()
public function getContext(): RequestContext
{
return $this->context;
}
@@ -108,15 +109,15 @@ 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;
}
/**
* {@inheritdoc}
*/
public function isStrictRequirements()
public function isStrictRequirements(): ?bool
{
return $this->strictRequirements;
}
@@ -124,16 +125,40 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
/**
* {@inheritdoc}
*/
public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
{
if (null === $route = $this->routes->get($name)) {
$route = null;
$locale = $parameters['_locale']
?? $this->context->getParameter('_locale')
?: $this->defaultLocale;
if (null !== $locale) {
do {
if (null !== ($route = $this->routes->get($name.'.'.$locale)) && $route->getDefault('_canonical_route') === $name) {
break;
}
} while (false !== $locale = strstr($locale, '_', true));
}
if (null === $route = $route ?? $this->routes->get($name)) {
throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name));
}
// 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());
}
/**
@@ -141,7 +166,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
* @throws InvalidParameterException When a parameter value for a placeholder is not correct because
* it does not match the requirement
*/
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array())
protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parameters, string $name, int $referenceType, array $hostTokens, array $requiredSchemes = []): string
{
$variables = array_flip($variables);
$mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters);
@@ -156,21 +181,25 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
$message = 'Parameter "{parameter}" for route "{route}" must match "{expected}" ("{given}" given) to generate a corresponding URL.';
foreach ($tokens as $token) {
if ('variable' === $token[0]) {
if (!$optional || !array_key_exists($token[3], $defaults) || null !== $mergedParams[$token[3]] && (string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]]) {
// check requirement
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#'.(empty($token[4]) ? '' : 'u'), $mergedParams[$token[3]])) {
$varName = $token[3];
// variable is not important by default
$important = $token[5] ?? false;
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 ($this->strictRequirements) {
throw new InvalidParameterException(strtr($message, array('{parameter}' => $token[3], '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$token[3]])));
throw new InvalidParameterException(strtr($message, ['{parameter}' => $varName, '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$varName]]));
}
if ($this->logger) {
$this->logger->error($message, array('parameter' => $token[3], 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$token[3]]));
$this->logger->error($message, ['parameter' => $varName, 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$varName]]);
}
return;
return '';
}
$url = $token[1].$mergedParams[$token[3]].$url;
$url = $token[1].$mergedParams[$varName].$url;
$optional = false;
}
} else {
@@ -190,63 +219,65 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
// the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3
// so we need to encode them as they are not used for this purpose here
// otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route
$url = strtr($url, array('/../' => '/%2E%2E/', '/./' => '/%2E/'));
if ('/..' === substr($url, -3)) {
$url = strtr($url, ['/../' => '/%2E%2E/', '/./' => '/%2E/']);
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';
}
$schemeAuthority = '';
if ($host = $this->context->getHost()) {
$scheme = $this->context->getScheme();
$host = $this->context->getHost();
$scheme = $this->context->getScheme();
if ($requiredSchemes) {
if (!in_array($scheme, $requiredSchemes, true)) {
$referenceType = self::ABSOLUTE_URL;
$scheme = current($requiredSchemes);
}
if ($requiredSchemes) {
if (!\in_array($scheme, $requiredSchemes, true)) {
$referenceType = self::ABSOLUTE_URL;
$scheme = current($requiredSchemes);
}
}
if ($hostTokens) {
$routeHost = '';
foreach ($hostTokens as $token) {
if ('variable' === $token[0]) {
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#i'.(empty($token[4]) ? '' : 'u'), $mergedParams[$token[3]])) {
if ($this->strictRequirements) {
throw new InvalidParameterException(strtr($message, array('{parameter}' => $token[3], '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$token[3]])));
}
if ($this->logger) {
$this->logger->error($message, array('parameter' => $token[3], 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$token[3]]));
}
return;
if ($hostTokens) {
$routeHost = '';
foreach ($hostTokens as $token) {
if ('variable' === $token[0]) {
// 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 ($this->strictRequirements) {
throw new InvalidParameterException(strtr($message, ['{parameter}' => $token[3], '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$token[3]]]));
}
$routeHost = $token[1].$mergedParams[$token[3]].$routeHost;
} else {
$routeHost = $token[1].$routeHost;
}
}
if ($this->logger) {
$this->logger->error($message, ['parameter' => $token[3], 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$token[3]]]);
}
if ($routeHost !== $host) {
$host = $routeHost;
if (self::ABSOLUTE_URL !== $referenceType) {
$referenceType = self::NETWORK_PATH;
return '';
}
$routeHost = $token[1].$mergedParams[$token[3]].$routeHost;
} else {
$routeHost = $token[1].$routeHost;
}
}
if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
if ($routeHost !== $host) {
$host = $routeHost;
if (self::ABSOLUTE_URL !== $referenceType) {
$referenceType = self::NETWORK_PATH;
}
}
}
if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
if ('' !== $host || ('' !== $scheme && 'http' !== $scheme && 'https' !== $scheme)) {
$port = '';
if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
if ('http' === $scheme && 80 !== $this->context->getHttpPort()) {
$port = ':'.$this->context->getHttpPort();
} elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) {
} elseif ('https' === $scheme && 443 !== $this->context->getHttpsPort()) {
$port = ':'.$this->context->getHttpsPort();
}
$schemeAuthority = self::NETWORK_PATH === $referenceType ? '//' : "$scheme://";
$schemeAuthority = self::NETWORK_PATH === $referenceType || '' === $scheme ? '//' : "$scheme://";
$schemeAuthority .= $host.$port;
}
}
@@ -262,25 +293,31 @@ 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 = '';
if (isset($defaults['_fragment'])) {
$fragment = $defaults['_fragment'];
}
$fragment = $defaults['_fragment'] ?? '';
if (isset($extra['_fragment'])) {
$fragment = $extra['_fragment'];
unset($extra['_fragment']);
}
if ($extra && $query = http_build_query($extra, '', '&', PHP_QUERY_RFC3986)) {
// "/" and "?" can be left decoded for better user experience, see
// http://tools.ietf.org/html/rfc3986#section-3.4
$url .= '?'.strtr($query, array('%2F' => '/'));
if ($extra && $query = http_build_query($extra, '', '&', \PHP_QUERY_RFC3986)) {
$url .= '?'.strtr($query, self::QUERY_FRAGMENT_DECODED);
}
if ('' !== $fragment) {
$url .= '#'.strtr(rawurlencode($fragment), array('%2F' => '/', '%3F' => '?'));
$url .= '#'.strtr(rawurlencode($fragment), self::QUERY_FRAGMENT_DECODED);
}
return $url;
@@ -303,10 +340,8 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
*
* @param string $basePath The base path
* @param string $targetPath The target path
*
* @return string The relative target path
*/
public static function getRelativePath($basePath, $targetPath)
public static function getRelativePath(string $basePath, string $targetPath): string
{
if ($basePath === $targetPath) {
return '';
@@ -326,7 +361,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
}
$targetDirs[] = $targetFile;
$path = str_repeat('../', count($sourceDirs)).implode('/', $targetDirs);
$path = str_repeat('../', \count($sourceDirs)).implode('/', $targetDirs);
// A reference to the same base directory or an empty subdirectory must be prefixed with "./".
// This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used

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,10 @@ 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
*
* @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 = array(), $referenceType = self::ABSOLUTE_PATH);
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string;
}

View File

@@ -1,4 +1,4 @@
Copyright (c) 2004-2017 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

@@ -12,16 +12,17 @@
namespace Symfony\Component\Routing\Loader;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
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\Config\Loader\LoaderResolverInterface;
* 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\Config\Loader\LoaderResolverInterface;
* public function index()
* {
* }
*
* /**
* * @Route("/{id}", name="blog_post", requirements = {"id" = "\d+"})
* * /
@@ -52,42 +51,50 @@ use Symfony\Component\Config\Loader\LoaderResolverInterface;
* {
* }
* }
*
* 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
{
/**
* @var Reader
*/
protected $reader;
protected $env;
/**
* @var string
*/
protected $routeAnnotationClass = 'Symfony\\Component\\Routing\\Annotation\\Route';
protected $routeAnnotationClass = RouteAnnotation::class;
/**
* @var int
*/
protected $defaultRouteIndex = 0;
/**
* Constructor.
*
* @param Reader $reader
*/
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;
}
@@ -95,14 +102,9 @@ abstract class AnnotationClassLoader implements LoaderInterface
/**
* Loads from annotations from a class.
*
* @param string $class A class name
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
*
* @throws \InvalidArgumentException When route can't be parsed
*/
public function load($class, $type = null)
public function load(mixed $class, string $type = null): RouteCollection
{
if (!class_exists($class)) {
throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
@@ -118,37 +120,52 @@ 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') && $annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) {
$globals['path'] = '';
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
$globals = $this->resetGlobals();
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);
}
$name = $globals['name'].$name;
$defaults = array_replace($globals['defaults'], $annot->getDefaults());
foreach ($method->getParameters() as $param) {
if (false !== strpos($globals['path'].$annot->getPath(), sprintf('{%s}', $param->getName())) && !isset($defaults[$param->getName()]) && $param->isDefaultValueAvailable()) {
$defaults[$param->getName()] = $param->getDefaultValue();
$requirements = $annot->getRequirements();
foreach ($requirements as $placeholder => $requirement) {
if (\is_int($placeholder)) {
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()));
}
}
$requirements = array_replace($globals['requirements'], $annot->getRequirements());
$defaults = array_replace($globals['defaults'], $annot->getDefaults());
$requirements = array_replace($globals['requirements'], $requirements);
$options = array_replace($globals['options'], $annot->getOptions());
$schemes = array_merge($globals['schemes'], $annot->getSchemes());
$methods = array_merge($globals['methods'], $annot->getMethods());
@@ -158,24 +175,69 @@ 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'];
$paths = [];
if (\is_array($path)) {
if (!\is_array($prefix)) {
foreach ($path as $locale => $localePath) {
$paths[$locale] = $prefix.$localePath;
}
} elseif ($missing = array_diff_key($prefix, $path)) {
throw new \LogicException(sprintf('Route to "%s" is missing paths for locale(s) "%s".', $class->name.'::'.$method->name, implode('", "', array_keys($missing))));
} else {
foreach ($path as $locale => $localePath) {
if (!isset($prefix[$locale])) {
throw new \LogicException(sprintf('Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".', $method->name, $locale, $class->name));
}
$paths[$locale] = $prefix[$locale].$localePath;
}
}
} elseif (\is_array($prefix)) {
foreach ($prefix as $locale => $localePrefix) {
$paths[$locale] = $localePrefix.$path;
}
} else {
$paths[] = $prefix.$path;
}
$route = $this->createRoute($globals['path'].$annot->getPath(), $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
foreach ($method->getParameters() as $param) {
if (isset($defaults[$param->name]) || !$param->isDefaultValueAvailable()) {
continue;
}
foreach ($paths as $locale => $path) {
if (preg_match(sprintf('/\{%s(?:<.*?>)?\}/', preg_quote($param->name)), $path)) {
$defaults[$param->name] = $param->getDefaultValue();
break;
}
}
}
$this->configureRoute($route, $class, $method, $annot);
$collection->add($name, $route);
foreach ($paths as $locale => $path) {
$route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
$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, $priority);
} else {
$collection->add($name, $route, $priority);
}
}
}
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports(mixed $resource, string $type = null): bool
{
return is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || 'annotation' === $type);
return \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || 'annotation' === $type);
}
/**
@@ -188,21 +250,19 @@ abstract class AnnotationClassLoader implements LoaderInterface
/**
* {@inheritdoc}
*/
public function getResolver()
public function getResolver(): LoaderResolverInterface
{
}
/**
* Gets the default route name for a class method.
*
* @param \ReflectionClass $class
* @param \ReflectionMethod $method
*
* @return string
*/
protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method)
{
$name = strtolower(str_replace('\\', '_', $class->name).'_'.$method->name);
$name = str_replace('\\', '_', $class->name).'_'.$method->name;
$name = \function_exists('mb_strtolower') && preg_match('//u', $name) ? mb_strtolower($name, 'UTF-8') : strtolower($name);
if ($this->defaultRouteIndex > 0) {
$name .= '_'.$this->defaultRouteIndex;
}
@@ -213,22 +273,27 @@ abstract class AnnotationClassLoader implements LoaderInterface
protected function getGlobals(\ReflectionClass $class)
{
$globals = array(
'path' => '',
'requirements' => array(),
'options' => array(),
'defaults' => array(),
'schemes' => array(),
'methods' => array(),
'host' => '',
'condition' => '',
);
$globals = $this->resetGlobals();
$annot = null;
if ($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();
}
if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) {
if (null !== $annot->getPath()) {
$globals['path'] = $annot->getPath();
}
$globals['localized_paths'] = $annot->getLocalizedPaths();
if (null !== $annot->getRequirements()) {
$globals['requirements'] = $annot->getRequirements();
}
@@ -256,15 +321,68 @@ abstract class AnnotationClassLoader implements LoaderInterface
if (null !== $annot->getCondition()) {
$globals['condition'] = $annot->getCondition();
}
$globals['priority'] = $annot->getPriority() ?? 0;
$globals['env'] = $annot->getEnv();
foreach ($globals['requirements'] as $placeholder => $requirement) {
if (\is_int($placeholder)) {
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()));
}
}
}
return $globals;
}
protected function createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition)
private function resetGlobals(): array
{
return [
'path' => null,
'localized_paths' => [],
'requirements' => [],
'options' => [],
'defaults' => [],
'schemes' => [],
'methods' => [],
'host' => '',
'condition' => '',
'name' => '',
'priority' => 0,
'env' => null,
];
}
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
{
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

@@ -11,8 +11,8 @@
namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Routing\RouteCollection;
/**
* AnnotationDirectoryLoader loads routing information from annotations set
@@ -23,18 +23,13 @@ use Symfony\Component\Config\Resource\DirectoryResource;
class AnnotationDirectoryLoader extends AnnotationFileLoader
{
/**
* Loads from annotations from a directory.
*
* @param string $path A directory path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
*
* @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed
*/
public function load($path, $type = null)
public function load(mixed $path, string $type = null): ?RouteCollection
{
$dir = $this->locator->locate($path);
if (!is_dir($dir = $this->locator->locate($path))) {
return parent::supports($path, $type) ? parent::load($path, $type) : new RouteCollection();
}
$collection = new RouteCollection();
$collection->addResource(new DirectoryResource($dir, '/\.php$/'));
@@ -52,7 +47,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;
}
@@ -72,18 +67,20 @@ class AnnotationDirectoryLoader extends AnnotationFileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports(mixed $resource, string $type = null): bool
{
if (!is_string($resource)) {
if ('annotation' === $type) {
return true;
}
if ($type || !\is_string($resource)) {
return false;
}
try {
$path = $this->locator->locate($resource);
return is_dir($this->locator->locate($resource));
} catch (\Exception $e) {
return false;
}
return is_dir($path) && (!$type || 'annotation' === $type);
}
}

View File

@@ -11,10 +11,10 @@
namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\RouteCollection;
/**
* AnnotationFileLoader loads routing information from annotations set
@@ -26,18 +26,10 @@ class AnnotationFileLoader extends FileLoader
{
protected $loader;
/**
* Constructor.
*
* @param FileLocatorInterface $locator A FileLocator instance
* @param AnnotationClassLoader $loader An AnnotationClassLoader instance
*
* @throws \RuntimeException
*/
public function __construct(FileLocatorInterface $locator, AnnotationClassLoader $loader)
{
if (!function_exists('token_get_all')) {
throw new \RuntimeException('The Tokenizer extension is required for the routing annotation loaders.');
if (!\function_exists('token_get_all')) {
throw new \LogicException('The Tokenizer extension is required for the routing annotation loaders.');
}
parent::__construct($locator);
@@ -48,26 +40,24 @@ class AnnotationFileLoader extends FileLoader
/**
* Loads from annotations from a file.
*
* @param string $file A PHP file path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
*
* @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed
*/
public function load($file, $type = null)
public function load(mixed $file, string $type = null): ?RouteCollection
{
$path = $this->locator->locate($file);
$collection = new RouteCollection();
if ($class = $this->findClass($path)) {
$refl = new \ReflectionClass($class);
if ($refl->isAbstract()) {
return null;
}
$collection->addResource(new FileResource($path));
$collection->addCollection($this->loader->load($class, $type));
}
if (\PHP_VERSION_ID >= 70000) {
// PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098
gc_mem_caches();
}
gc_mem_caches();
return $collection;
}
@@ -75,69 +65,71 @@ class AnnotationFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports(mixed $resource, string $type = null): bool
{
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
*/
protected function findClass($file)
protected function findClass(string $file): string|false
{
$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], array(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]) {
// Skip usage of ::class constant
$isClassConstant = false;
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]) {
$isClassConstant = true;
if (\T_DOUBLE_COLON === $tokens[$j][0] || \T_NEW === $tokens[$j][0]) {
$skipClassToken = true;
break;
} elseif (!in_array($tokens[$j][0], array(T_WHITESPACE, T_DOC_COMMENT, T_COMMENT))) {
} elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) {
break;
}
}
if (!$isClassConstant) {
if (!$skipClassToken) {
$class = true;
}
}
if (T_NAMESPACE === $token[0]) {
if (\T_NAMESPACE === $token[0]) {
$namespace = true;
}
}

View File

@@ -25,21 +25,16 @@ class ClosureLoader extends Loader
{
/**
* Loads a Closure.
*
* @param \Closure $closure A Closure
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
*/
public function load($closure, $type = null)
public function load(mixed $closure, string $type = null): RouteCollection
{
return $closure();
return $closure($this->env);
}
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports(mixed $resource, string $type = null): bool
{
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): static
{
$this->alias->setDeprecated($package, $version, $message);
return $this;
}
}

View File

@@ -0,0 +1,122 @@
<?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\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class CollectionConfigurator
{
use Traits\AddTrait;
use Traits\HostTrait;
use Traits\RouteTrait;
private $parent;
private $parentConfigurator;
private ?array $parentPrefixes;
private string|array|null $host = null;
public function __construct(RouteCollection $parent, string $name, self $parentConfigurator = null, array $parentPrefixes = null)
{
$this->parent = $parent;
$this->name = $name;
$this->collection = new RouteCollection();
$this->route = new Route('');
$this->parentConfigurator = $parentConfigurator; // for GC control
$this->parentPrefixes = $parentPrefixes;
}
public function __sleep(): array
{
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.
*/
final public function collection(string $name = ''): self
{
return new self($this->collection, $this->name.$name, $this, $this->prefixes);
}
/**
* Sets the prefix to add to the path of all child routes.
*
* @param string|array $prefix the prefix, or the localized prefixes
*
* @return $this
*/
final public function prefix(string|array $prefix): static
{
if (\is_array($prefix)) {
if (null === $this->parentPrefixes) {
// no-op
} elseif ($missing = array_diff_key($this->parentPrefixes, $prefix)) {
throw new \LogicException(sprintf('Collection "%s" is missing prefixes for locale(s) "%s".', $this->name, implode('", "', array_keys($missing))));
} else {
foreach ($prefix as $locale => $localePrefix) {
if (!isset($this->parentPrefixes[$locale])) {
throw new \LogicException(sprintf('Collection "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $this->name, $locale));
}
$prefix[$locale] = $this->parentPrefixes[$locale].$localePrefix;
}
}
$this->prefixes = $prefix;
$this->route->setPath('/');
} else {
$this->prefixes = null;
$this->route->setPath($prefix);
}
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(string|array $host): static
{
$this->host = $host;
return $this;
}
private function createRoute(string $path): Route
{
return (clone $this->route)->setPath($path);
}
}

View File

@@ -0,0 +1,87 @@
<?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\Routing\RouteCollection;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class ImportConfigurator
{
use Traits\HostTrait;
use Traits\PrefixTrait;
use Traits\RouteTrait;
private $parent;
public function __construct(RouteCollection $parent, RouteCollection $route)
{
$this->parent = $parent;
$this->route = $route;
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->parent->addCollection($this->route);
}
/**
* Sets the prefix to add to the path of all child routes.
*
* @param string|array $prefix the prefix, or the localized prefixes
*
* @return $this
*/
final public function prefix(string|array $prefix, bool $trailingSlashOnRoot = true): static
{
$this->addPrefix($this->route, $prefix, $trailingSlashOnRoot);
return $this;
}
/**
* Sets the prefix to add to the name of all child routes.
*
* @return $this
*/
final public function namePrefix(string $namePrefix): static
{
$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(string|array $host): static
{
$this->addHost($this->route, $host);
return $this;
}
}

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;
use Symfony\Component\Routing\RouteCollection;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class RouteConfigurator
{
use Traits\AddTrait;
use Traits\HostTrait;
use Traits\RouteTrait;
protected $parentConfigurator;
public function __construct(RouteCollection $collection, RouteCollection $route, string $name = '', CollectionConfigurator $parentConfigurator = null, array $prefixes = null)
{
$this->collection = $collection;
$this->route = $route;
$this->name = $name;
$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(string|array $host): static
{
$this->addHost($this->route, $host);
return $this;
}
}

View File

@@ -0,0 +1,78 @@
<?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\Routing\Loader\PhpFileLoader;
use Symfony\Component\Routing\RouteCollection;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class RoutingConfigurator
{
use Traits\AddTrait;
private $loader;
private string $path;
private string $file;
private ?string $env;
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;
}
/**
* @param string|string[]|null $exclude Glob patterns to exclude from the import
*/
final public function import(string|array $resource, string $type = null, bool $ignoreErrors = false, string|array $exclude = null): ImportConfigurator
{
$this->loader->setCurrentDir(\dirname($this->path));
$imported = $this->loader->import($resource, $type, $ignoreErrors, $this->file, $exclude) ?: [];
if (!\is_array($imported)) {
return new ImportConfigurator($this->collection, $imported);
}
$mergedCollection = new RouteCollection();
foreach ($imported as $subCollection) {
$mergedCollection->addCollection($subCollection);
}
return new ImportConfigurator($this->collection, $mergedCollection);
}
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;
}
final public function withPath(string $path): static
{
$clone = clone $this;
$clone->path = $clone->file = $path;
return $clone;
}
}

View File

@@ -0,0 +1,60 @@
<?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\Loader\Configurator\AliasConfigurator;
use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator;
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
use Symfony\Component\Routing\RouteCollection;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
trait AddTrait
{
use LocalizedRouteTrait;
/**
* @var RouteCollection
*/
protected $collection;
protected $name = '';
protected $prefixes;
/**
* Adds a route.
*
* @param string|array $path the path, or the localized paths of the route
*/
public function add(string $name, string|array $path): RouteConfigurator
{
$parentConfigurator = $this instanceof CollectionConfigurator ? $this : ($this instanceof RouteConfigurator ? $this->parentConfigurator : null);
$route = $this->createLocalizedRoute($this->collection, $name, $path, $this->name, $this->prefixes);
return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes);
}
public function alias(string $name, string $alias): AliasConfigurator
{
return new AliasConfigurator($this->collection->addAlias($name, $alias));
}
/**
* Adds a route.
*
* @param string|array $path the path, or the localized paths of the route
*/
public function __invoke(string $name, string|array $path): RouteConfigurator
{
return $this->add($name, $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, string|array $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, string|array $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, string|array $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

@@ -0,0 +1,175 @@
<?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;
trait RouteTrait
{
/**
* @var RouteCollection|Route
*/
protected $route;
/**
* Adds defaults.
*
* @return $this
*/
final public function defaults(array $defaults): static
{
$this->route->addDefaults($defaults);
return $this;
}
/**
* Adds requirements.
*
* @return $this
*/
final public function requirements(array $requirements): static
{
$this->route->addRequirements($requirements);
return $this;
}
/**
* Adds options.
*
* @return $this
*/
final public function options(array $options): static
{
$this->route->addOptions($options);
return $this;
}
/**
* Whether paths should accept utf8 encoding.
*
* @return $this
*/
final public function utf8(bool $utf8 = true): static
{
$this->route->addOptions(['utf8' => $utf8]);
return $this;
}
/**
* Sets the condition.
*
* @return $this
*/
final public function condition(string $condition): static
{
$this->route->setCondition($condition);
return $this;
}
/**
* Sets the pattern for the host.
*
* @return $this
*/
final public function host(string $pattern): static
{
$this->route->setHost($pattern);
return $this;
}
/**
* Sets the schemes (e.g. 'https') this route is restricted to.
* So an empty array means that any scheme is allowed.
*
* @param string[] $schemes
*
* @return $this
*/
final public function schemes(array $schemes): static
{
$this->route->setSchemes($schemes);
return $this;
}
/**
* Sets the HTTP methods (e.g. 'POST') this route is restricted to.
* So an empty array means that any method is allowed.
*
* @param string[] $methods
*
* @return $this
*/
final public function methods(array $methods): static
{
$this->route->setMethods($methods);
return $this;
}
/**
* Adds the "_controller" entry to defaults.
*
* @param callable|string|array $controller a callable or parseable pseudo-callable
*
* @return $this
*/
final public function controller(callable|string|array $controller): static
{
$this->route->addDefaults(['_controller' => $controller]);
return $this;
}
/**
* Adds the "_locale" entry to defaults.
*
* @return $this
*/
final public function locale(string $locale): static
{
$this->route->addDefaults(['_locale' => $locale]);
return $this;
}
/**
* Adds the "_format" entry to defaults.
*
* @return $this
*/
final public function format(string $format): static
{
$this->route->addDefaults(['_format' => $format]);
return $this;
}
/**
* Adds the "_stateless" entry to defaults.
*
* @return $this
*/
final public function stateless(bool $stateless = true): static
{
$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(mixed $resource, string $type = null): bool
{
return 'service' === $type && \is_string($resource);
}
/**
* {@inheritdoc}
*/
protected function getObject(string $id): object
{
return $this->container->get($id);
}
}

View File

@@ -1,40 +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.
*
* This depends on the DependencyInjection component.
*
* @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

@@ -12,15 +12,15 @@
namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Routing\RouteCollection;
class DirectoryLoader extends FileLoader
{
/**
* {@inheritdoc}
*/
public function load($file, $type = null)
public function load(mixed $file, string $type = null): mixed
{
$path = $this->locator->locate($file);
@@ -49,7 +49,7 @@ class DirectoryLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports(mixed $resource, string $type = null): bool
{
// only when type is forced to directory, not to conflict with AnnotationLoader

View File

@@ -0,0 +1,47 @@
<?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 Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Routing\RouteCollection;
/**
* GlobFileLoader loads files from a glob pattern.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class GlobFileLoader extends FileLoader
{
/**
* {@inheritdoc}
*/
public function load(mixed $resource, string $type = null): mixed
{
$collection = new RouteCollection();
foreach ($this->glob($resource, false, $globResource) as $path => $info) {
$collection->addCollection($this->import($path));
}
$collection->addResource($globResource);
return $collection;
}
/**
* {@inheritdoc}
*/
public function supports(mixed $resource, string $type = null): bool
{
return 'glob' === $type;
}
}

View File

@@ -0,0 +1,77 @@
<?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 Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\RouteCollection;
/**
* A route loader that calls a method on an object to load the routes.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
abstract class ObjectLoader extends Loader
{
/**
* Returns the object that the method will be called on to load routes.
*
* For example, if your application uses a service container,
* the $id may be a service id.
*/
abstract protected function getObject(string $id): object;
/**
* Calls the object method that will load the routes.
*/
public function load(mixed $resource, string $type = null): RouteCollection
{
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);
$method = $parts[1] ?? '__invoke';
$loaderObject = $this->getObject($parts[0]);
if (!\is_object($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_debug_type($loaderObject), $resource));
}
$routeCollection = $loaderObject->$method($this, $this->env);
if (!$routeCollection instanceof RouteCollection) {
$type = get_debug_type($routeCollection);
throw new \LogicException(sprintf('The "%s::%s()" method must return a RouteCollection: "%s" returned.', get_debug_type($loaderObject), $method, $type));
}
// make the object file tracked so that if it changes, the cache rebuilds
$this->addClassResource(new \ReflectionClass($loaderObject), $routeCollection);
return $routeCollection;
}
private function addClassResource(\ReflectionClass $class, RouteCollection $collection)
{
do {
if (is_file($class->getFileName())) {
$collection->addResource(new FileResource($class->getFileName()));
}
} while ($class = $class->getParentClass());
}
}

View File

@@ -1,95 +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;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\RouteCollection;
/**
* A route loader that calls a method on an object to load the routes.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
abstract class ObjectRouteLoader extends Loader
{
/**
* Returns the object that the method will be called on to load routes.
*
* 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);
/**
* Calls the service that will load the routes.
*
* @param mixed $resource Some value that will resolve to a callable
* @param string|null $type The resource type
*
* @return RouteCollection
*/
public function load($resource, $type = null)
{
$parts = explode(':', $resource);
if (count($parts) != 2) {
throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the "service" route loader: use the format "service_name:methodName"', $resource));
}
$serviceString = $parts[0];
$method = $parts[1];
$loaderObject = $this->getServiceObject($serviceString);
if (!is_object($loaderObject)) {
throw new \LogicException(sprintf('%s:getServiceObject() must return an object: %s returned', get_class($this), gettype($loaderObject)));
}
if (!method_exists($loaderObject, $method)) {
throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s"', $method, get_class($loaderObject), $resource));
}
$routeCollection = call_user_func(array($loaderObject, $method), $this);
if (!$routeCollection instanceof RouteCollection) {
$type = is_object($routeCollection) ? get_class($routeCollection) : gettype($routeCollection);
throw new \LogicException(sprintf('The %s::%s method must return a RouteCollection: %s returned', get_class($loaderObject), $method, $type));
}
// make the service 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 {
if (is_file($class->getFileName())) {
$collection->addResource(new FileResource($class->getFileName()));
}
} while ($class = $class->getParentClass());
}
}

View File

@@ -13,6 +13,7 @@ namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\Component\Routing\RouteCollection;
/**
@@ -21,23 +22,33 @@ 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
{
/**
* Loads a PHP file.
*
* @param string $file A PHP file path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
*/
public function load($file, $type = null)
public function load(mixed $file, string $type = null): RouteCollection
{
$path = $this->locator->locate($file);
$this->setCurrentDir(dirname($path));
$this->setCurrentDir(\dirname($path));
// the closure forbids access to the private scope in the included file
$loader = $this;
$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 = $this->callConfigurator($result, $path, $file);
} else {
$collection = $result;
}
$collection = self::includeFile($path, $this);
$collection->addResource(new FileResource($path));
return $collection;
@@ -46,21 +57,24 @@ class PhpFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports(mixed $resource, string $type = null): bool
{
return is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'php' === $type);
return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'php' === $type);
}
/**
* Safe include. Used for scope isolation.
*
* @param string $file File to include
* @param PhpFileLoader $loader the loader variable is exposed to the included file below
*
* @return RouteCollection
*/
private static function includeFile($file, PhpFileLoader $loader)
protected function callConfigurator(callable $result, string $path, string $file): RouteCollection
{
return include $file;
$collection = new RouteCollection();
$result(new RoutingConfigurator($collection, $this, $path, $file, $this->env));
return $collection;
}
}
/**
* @internal
*/
final class ProtectedPhpFileLoader extends PhpFileLoader
{
}

View File

@@ -11,11 +11,13 @@
namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Util\XmlUtils;
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;
/**
* XmlFileLoader loads XML routing files.
@@ -25,21 +27,18 @@ use Symfony\Component\Config\Util\XmlUtils;
*/
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.
*
* @param string $file An XML file path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
*
* @throws \InvalidArgumentException When the file cannot be loaded or when the XML cannot be
* parsed because it does not validate against the scheme.
* @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(mixed $file, string $type = null): RouteCollection
{
$path = $this->locator->locate($file);
@@ -63,14 +62,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 +77,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,46 +95,64 @@ class XmlFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports(mixed $resource, string $type = null): bool
{
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')) || !$node->hasAttribute('path')) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" and a "path" attribute.', $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) = $this->parseConfigs($node, $path);
if ($deprecationInfo = $this->parseDeprecation($node, $path)) {
$alias->setDeprecated($deprecationInfo['package'], $deprecationInfo['version'], $deprecationInfo['message']);
}
$route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition);
$collection->add($id, $route);
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));
}
if ($paths && '' !== $node->getAttribute('path')) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "path" attribute and <path> child nodes.', $path));
}
$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));
@@ -138,48 +160,73 @@ 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) = $this->parseConfigs($node, $path);
[$defaults, $requirements, $options, $condition, /* $paths */, $prefixes, $hosts] = $this->parseConfigs($node, $path);
$this->setCurrentDir(dirname($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));
}
$subCollection = $this->import($resource, ('' !== $type ? $type : null), false, $file);
/* @var $subCollection RouteCollection */
$subCollection->addPrefix($prefix);
if (null !== $host) {
$subCollection->setHost($host);
$exclude = [];
foreach ($node->childNodes as $child) {
if ($child instanceof \DOMElement && $child->localName === $exclude && self::NAMESPACE_URI === $child->namespaceURI) {
$exclude[] = $child->nodeValue;
}
}
if (null !== $condition) {
$subCollection->setCondition($condition);
}
if (null !== $schemes) {
$subCollection->setSchemes($schemes);
}
if (null !== $methods) {
$subCollection->setMethods($methods);
}
$subCollection->addDefaults($defaults);
$subCollection->addRequirements($requirements);
$subCollection->addOptions($options);
$collection->addCollection($subCollection);
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, $exclude) ?: [];
if (!\is_array($imported)) {
$imported = [$imported];
}
foreach ($imported as $subCollection) {
$this->addPrefix($subCollection, $prefixes ?: $prefix, $trailingSlashOnRoot);
if (null !== $hosts) {
$this->addHost($subCollection, $hosts);
}
if (null !== $condition) {
$subCollection->setCondition($condition);
}
if (null !== $schemes) {
$subCollection->setSchemes($schemes);
}
if (null !== $methods) {
$subCollection->setMethods($methods);
}
if (null !== $namePrefix) {
$subCollection->addNamePrefix($namePrefix);
}
$subCollection->addDefaults($defaults);
$subCollection->addRequirements($requirements);
$subCollection->addOptions($options);
$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): \DOMDocument
{
return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH);
}
@@ -187,26 +234,34 @@ 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 = array();
$requirements = array();
$options = array();
$defaults = [];
$requirements = [];
$options = [];
$condition = null;
$prefixes = [];
$paths = [];
$hosts = [];
/** @var \DOMElement $n */
foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) {
if ($node !== $n->parentNode) {
continue;
}
switch ($n->localName) {
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;
case 'default':
if ($this->isElementValueNull($n)) {
$defaults[$n->getAttribute('key')] = null;
@@ -219,7 +274,7 @@ class XmlFileLoader extends FileLoader
$requirements[$n->getAttribute('key')] = trim($n->textContent);
break;
case 'option':
$options[$n->getAttribute('key')] = trim($n->textContent);
$options[$n->getAttribute('key')] = XmlUtils::phpize(trim($n->textContent));
break;
case 'condition':
$condition = trim($n->textContent);
@@ -229,21 +284,48 @@ class XmlFileLoader extends FileLoader
}
}
return array($defaults, $requirements, $options, $condition);
if ($controller = $node->getAttribute('controller')) {
if (isset($defaults['_controller'])) {
$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 ', $path).$name);
}
$defaults['_controller'] = $controller;
}
if ($node->hasAttribute('locale')) {
$defaults['_locale'] = $node->getAttribute('locale');
}
if ($node->hasAttribute('format')) {
$defaults['_format'] = $node->getAttribute('format');
}
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);
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
*/
private function parseDefaultsConfig(\DOMElement $element, $path)
private function parseDefaultsConfig(\DOMElement $element, string $path): array|bool|float|int|string|null
{
if ($this->isElementValueNull($element)) {
return;
return null;
}
// Check for existing element nodes in the default element. There can
@@ -270,17 +352,12 @@ 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
*
* @throws \InvalidArgumentException when the XML is invalid
*/
private function parseDefaultNode(\DOMElement $node, $path)
private function parseDefaultNode(\DOMElement $node, string $path): array|bool|float|int|string|null
{
if ($this->isElementValueNull($node)) {
return;
return null;
}
switch ($node->localName) {
@@ -293,7 +370,7 @@ class XmlFileLoader extends FileLoader
case 'string':
return trim($node->nodeValue);
case 'list':
$list = array();
$list = [];
foreach ($node->childNodes as $element) {
if (!$element instanceof \DOMElement) {
@@ -309,7 +386,7 @@ class XmlFileLoader extends FileLoader
return $list;
case 'map':
$map = array();
$map = [];
foreach ($node->childNodes as $element) {
if (!$element instanceof \DOMElement) {
@@ -329,7 +406,7 @@ class XmlFileLoader extends FileLoader
}
}
private function isElementValueNull(\DOMElement $element)
private function isElementValueNull(\DOMElement $element): bool
{
$namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance';
@@ -339,4 +416,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

@@ -11,12 +11,14 @@
namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Resource\FileResource;
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;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Yaml\Yaml;
/**
@@ -27,22 +29,19 @@ use Symfony\Component\Yaml\Yaml;
*/
class YamlFileLoader extends FileLoader
{
private static $availableKeys = array(
'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition',
);
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;
/**
* Loads a Yaml file.
*
* @param string $file A Yaml file path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
*
* @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid
*/
public function load($file, $type = null)
public function load(mixed $file, string $type = null): RouteCollection
{
$path = $this->locator->locate($file);
@@ -54,14 +53,12 @@ class YamlFileLoader extends FileLoader
throw new \InvalidArgumentException(sprintf('File "%s" not found.', $path));
}
if (null === $this->yamlParser) {
$this->yamlParser = new YamlParser();
}
$this->yamlParser ??= new YamlParser();
try {
$parsedConfig = $this->yamlParser->parse(file_get_contents($path), Yaml::PARSE_KEYS_AS_STRINGS);
$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();
@@ -73,11 +70,29 @@ class YamlFileLoader extends FileLoader
}
// not an array
if (!is_array($parsedConfig)) {
if (!\is_array($parsedConfig)) {
throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $path));
}
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,116 +108,192 @@ class YamlFileLoader extends FileLoader
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
public function supports(mixed $resource, string $type = null): bool
{
return is_string($resource) && in_array(pathinfo($resource, PATHINFO_EXTENSION), array('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'] : array();
$requirements = isset($config['requirements']) ? $config['requirements'] : array();
$options = isset($config['options']) ? $config['options'] : array();
$host = isset($config['host']) ? $config['host'] : '';
$schemes = isset($config['schemes']) ? $config['schemes'] : array();
$methods = isset($config['methods']) ? $config['methods'] : array();
$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'] ?? ''
);
}
$route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
return;
}
$collection->add($name, $route);
$defaults = $config['defaults'] ?? [];
$requirements = $config['requirements'] ?? [];
$options = $config['options'] ?? [];
foreach ($requirements as $placeholder => $requirement) {
if (\is_int($placeholder)) {
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));
}
}
if (isset($config['controller'])) {
$defaults['_controller'] = $config['controller'];
}
if (isset($config['locale'])) {
$defaults['_locale'] = $config['locale'];
}
if (isset($config['format'])) {
$defaults['_format'] = $config['format'];
}
if (isset($config['utf8'])) {
$options['utf8'] = $config['utf8'];
}
if (isset($config['stateless'])) {
$defaults['_stateless'] = $config['stateless'];
}
$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);
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'] : array();
$requirements = isset($config['requirements']) ? $config['requirements'] : array();
$options = isset($config['options']) ? $config['options'] : array();
$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;
$this->setCurrentDir(dirname($path));
if (isset($config['controller'])) {
$defaults['_controller'] = $config['controller'];
}
if (isset($config['locale'])) {
$defaults['_locale'] = $config['locale'];
}
if (isset($config['format'])) {
$defaults['_format'] = $config['format'];
}
if (isset($config['utf8'])) {
$options['utf8'] = $config['utf8'];
}
if (isset($config['stateless'])) {
$defaults['_stateless'] = $config['stateless'];
}
$subCollection = $this->import($config['resource'], $type, false, $file);
/* @var $subCollection RouteCollection */
$subCollection->addPrefix($prefix);
if (null !== $host) {
$subCollection->setHost($host);
}
if (null !== $condition) {
$subCollection->setCondition($condition);
}
if (null !== $schemes) {
$subCollection->setSchemes($schemes);
}
if (null !== $methods) {
$subCollection->setMethods($methods);
}
$subCollection->addDefaults($defaults);
$subCollection->addRequirements($requirements);
$subCollection->addOptions($options);
$this->setCurrentDir(\dirname($path));
$collection->addCollection($subCollection);
/** @var RouteCollection[] $imported */
$imported = $this->import($config['resource'], $type, false, $file, $exclude) ?: [];
if (!\is_array($imported)) {
$imported = [$imported];
}
foreach ($imported as $subCollection) {
$this->addPrefix($subCollection, $prefix, $trailingSlashOnRoot);
if (null !== $host) {
$this->addHost($subCollection, $host);
}
if (null !== $condition) {
$subCollection->setCondition($condition);
}
if (null !== $schemes) {
$subCollection->setSchemes($schemes);
}
if (null !== $methods) {
$subCollection->setMethods($methods);
}
if (null !== $namePrefix) {
$subCollection->addNamePrefix($namePrefix);
}
$subCollection->addDefaults($defaults);
$subCollection->addRequirements($requirements);
$subCollection->addOptions($options);
$collection->addCollection($subCollection);
}
}
/**
* Validates the route configuration.
*
* @param array $config A resource config
* @param string $name The config key
* @param string $path The loaded file path
*
* @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(mixed $config, string $name, string $path)
{
if (!is_array($config)) {
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
));
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));
}
if (!isset($config['resource']) && isset($config['type'])) {
throw new \InvalidArgumentException(sprintf(
'The "type" key for the route definition "%s" in "%s" is unsupported. It is only available for imports in combination with the "resource" key.',
$name, $path
));
throw new \InvalidArgumentException(sprintf('The "type" key for the route definition "%s" in "%s" is unsupported. It is only available for imports in combination with the "resource" key.', $name, $path));
}
if (!isset($config['resource']) && !isset($config['path'])) {
throw new \InvalidArgumentException(sprintf(
'You must define a "path" for the route "%s" in file "%s".',
$name, $path
));
throw new \InvalidArgumentException(sprintf('You must define a "path" for the route "%s" in file "%s".', $name, $path));
}
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,26 @@
<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">
<xsd:attribute name="locale" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:group name="configs">
<xsd:choice>
<xsd:element name="default" nillable="true" type="default" />
@@ -34,24 +51,46 @@
</xsd:group>
<xsd:complexType name="route">
<xsd:group ref="configs" minOccurs="0" maxOccurs="unbounded" />
<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" use="required" />
<xsd:attribute name="path" type="xsd:string" />
<xsd:attribute name="host" type="xsd:string" />
<xsd:attribute name="schemes" type="xsd:string" />
<xsd:attribute name="methods" type="xsd:string" />
<xsd:attribute name="controller" type="xsd:string" />
<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:group ref="configs" minOccurs="0" maxOccurs="unbounded" />
<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" />
<xsd:attribute name="schemes" type="xsd:string" />
<xsd:attribute name="methods" type="xsd:string" />
<xsd:attribute name="controller" type="xsd:string" />
<xsd:attribute name="locale" type="xsd:string" />
<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">
@@ -143,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

@@ -0,0 +1,31 @@
<?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\Routing\Matcher\Dumper\CompiledUrlMatcherTrait;
use Symfony\Component\Routing\RequestContext;
/**
* Matches URLs based on rules dumped by CompiledUrlMatcherDumper.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class CompiledUrlMatcher extends UrlMatcher
{
use CompiledUrlMatcherTrait;
public function __construct(array $compiledRoutes, RequestContext $context)
{
$this->context = $context;
[$this->matchHost, $this->staticRoutes, $this->regexpList, $this->dynamicRoutes, $this->checkCondition] = $compiledRoutes;
}
}

View File

@@ -0,0 +1,501 @@
<?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;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* CompiledUrlMatcherDumper creates PHP arrays to be used with CompiledUrlMatcher.
*
* @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>
*/
class CompiledUrlMatcherDumper extends MatcherDumper
{
private $expressionLanguage;
private ?\Exception $signalingException = null;
/**
* @var ExpressionFunctionProviderInterface[]
*/
private array $expressionLanguageProviders = [];
/**
* {@inheritdoc}
*/
public function dump(array $options = []): string
{
return <<<EOF
<?php
/**
* This file has been auto-generated
* by the Symfony Routing Component.
*/
return [
{$this->generateCompiledRoutes()}];
EOF;
}
public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
{
$this->expressionLanguageProviders[] = $provider;
}
/**
* Generates the arrays for CompiledUrlMatcher's constructor.
*/
public function getCompiledRoutes(bool $forDump = false): array
{
// Group hosts by same-suffix, re-order when possible
$matchHost = false;
$routes = new StaticPrefixCollection();
foreach ($this->getRoutes()->all() as $name => $route) {
if ($host = $route->getHost()) {
$matchHost = true;
$host = '/'.strtr(strrev($host), '}.{', '(/)');
}
$routes->addRoute($host ?: '/(.*)', [$name, $route]);
}
if ($matchHost) {
$compiledRoutes = [true];
$routes = $routes->populateCollection(new RouteCollection());
} else {
$compiledRoutes = [false];
$routes = $this->getRoutes();
}
[$staticRoutes, $dynamicRoutes] = $this->groupStaticRoutes($routes);
$conditions = [null];
$compiledRoutes[] = $this->compileStaticRoutes($staticRoutes, $conditions);
$chunkLimit = \count($dynamicRoutes);
while (true) {
try {
$this->signalingException = new \RuntimeException('Compilation failed: regular expression is too large');
$compiledRoutes = array_merge($compiledRoutes, $this->compileDynamicRoutes($dynamicRoutes, $matchHost, $chunkLimit, $conditions));
break;
} catch (\Exception $e) {
if (1 < $chunkLimit && $this->signalingException === $e) {
$chunkLimit = 1 + ($chunkLimit >> 1);
continue;
}
throw $e;
}
}
if ($forDump) {
$compiledRoutes[2] = $compiledRoutes[4];
}
unset($conditions[0]);
if ($conditions) {
foreach ($conditions as $expression => $condition) {
$conditions[$expression] = "case {$condition}: return {$expression};";
}
$checkConditionCode = <<<EOF
static function (\$condition, \$context, \$request) { // \$checkCondition
switch (\$condition) {
{$this->indent(implode("\n", $conditions), 3)}
}
}
EOF;
$compiledRoutes[4] = $forDump ? $checkConditionCode.",\n" : eval('return '.$checkConditionCode.';');
} else {
$compiledRoutes[4] = $forDump ? " null, // \$checkCondition\n" : null;
}
return $compiledRoutes;
}
private function generateCompiledRoutes(): string
{
[$matchHost, $staticRoutes, $regexpCode, $dynamicRoutes, $checkConditionCode] = $this->getCompiledRoutes(true);
$code = self::export($matchHost).', // $matchHost'."\n";
$code .= '[ // $staticRoutes'."\n";
foreach ($staticRoutes as $path => $routes) {
$code .= sprintf(" %s => [\n", self::export($path));
foreach ($routes as $route) {
$code .= sprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", ...array_map([__CLASS__, 'export'], $route));
}
$code .= " ],\n";
}
$code .= "],\n";
$code .= sprintf("[ // \$regexpList%s\n],\n", $regexpCode);
$code .= '[ // $dynamicRoutes'."\n";
foreach ($dynamicRoutes as $path => $routes) {
$code .= sprintf(" %s => [\n", self::export($path));
foreach ($routes as $route) {
$code .= sprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", ...array_map([__CLASS__, 'export'], $route));
}
$code .= " ],\n";
}
$code .= "],\n";
$code = preg_replace('/ => \[\n (\[.+?),\n \],/', ' => [$1],', $code);
return $this->indent($code, 1).$checkConditionCode;
}
/**
* Splits static routes from dynamic routes, so that they can be matched first, using a simple switch.
*/
private function groupStaticRoutes(RouteCollection $collection): array
{
$staticRoutes = $dynamicRegex = [];
$dynamicRoutes = new RouteCollection();
foreach ($collection->all() as $name => $route) {
$compiledRoute = $route->compile();
$staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/');
$hostRegex = $compiledRoute->getHostRegex();
$regex = $compiledRoute->getRegex();
if ($hasTrailingSlash = '/' !== $route->getPath()) {
$pos = strrpos($regex, '$');
$hasTrailingSlash = '/' === $regex[$pos - 1];
$regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash);
}
if (!$compiledRoute->getPathVariables()) {
$host = !$compiledRoute->getHostVariables() ? $route->getHost() : '';
$url = $route->getPath();
if ($hasTrailingSlash) {
$url = substr($url, 0, -1);
}
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;
}
}
$staticRoutes[$url][$name] = [$route, $hasTrailingSlash];
} else {
$dynamicRegex[] = [$hostRegex, $regex, $staticPrefix];
$dynamicRoutes->add($name, $route);
}
}
return [$staticRoutes, $dynamicRoutes];
}
/**
* Compiles static routes in a switch statement.
*
* Condition-less paths are put in a static array in the switch's default, with generic matching logic.
* Paths that can match two or more routes, or have user-specified conditions are put in separate switch's cases.
*
* @throws \LogicException
*/
private function compileStaticRoutes(array $staticRoutes, array &$conditions): array
{
if (!$staticRoutes) {
return [];
}
$compiledRoutes = [];
foreach ($staticRoutes as $url => $routes) {
$compiledRoutes[$url] = [];
foreach ($routes as $name => [$route, $hasTrailingSlash]) {
$compiledRoutes[$url][] = $this->compileRoute($route, $name, (!$route->compile()->getHostVariables() ? $route->getHost() : $route->compile()->getHostRegex()) ?: null, $hasTrailingSlash, false, $conditions);
}
}
return $compiledRoutes;
}
/**
* Compiles a regular expression followed by a switch statement to match dynamic routes.
*
* The regular expression matches both the host and the pathinfo at the same time. For stellar performance,
* it is built as a tree of patterns, with re-ordering logic to group same-prefix routes together when possible.
*
* Patterns are named so that we know which one matched (https://pcre.org/current/doc/html/pcre2syntax.html#SEC23).
* This name is used to "switch" to the additional logic required to match the final route.
*
* Condition-less paths are put in a static array in the switch's default, with generic matching logic.
* 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 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 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
{
if (!$collection->all()) {
return [[], [], ''];
}
$regexpList = [];
$code = '';
$state = (object) [
'regexMark' => 0,
'regex' => [],
'routes' => [],
'mark' => 0,
'markTail' => 0,
'hostVars' => [],
'vars' => [],
];
$state->getVars = static function ($m) use ($state) {
if ('_route' === $m[1]) {
return '?:';
}
$state->vars[] = $m[1];
return '';
};
$chunkSize = 0;
$prev = null;
$perModifiers = [];
foreach ($collection->all() as $name => $route) {
preg_match('#[a-zA-Z]*$#', $route->compile()->getRegex(), $rx);
if ($chunkLimit < ++$chunkSize || $prev !== $rx[0] && $route->compile()->getPathVariables()) {
$chunkSize = 1;
$routes = new RouteCollection();
$perModifiers[] = [$rx[0], $routes];
$prev = $rx[0];
}
$routes->add($name, $route);
}
foreach ($perModifiers as [$modifiers, $routes]) {
$prev = false;
$perHost = [];
foreach ($routes->all() as $name => $route) {
$regex = $route->compile()->getHostRegex();
if ($prev !== $regex) {
$routes = new RouteCollection();
$perHost[] = [$regex, $routes];
$prev = $regex;
}
$routes->add($name, $route);
}
$prev = false;
$rx = '{^(?';
$code .= "\n {$state->mark} => ".self::export($rx);
$startingMark = $state->mark;
$state->mark += \strlen($rx);
$state->regex = $rx;
foreach ($perHost as [$hostRegex, $routes]) {
if ($matchHost) {
if ($hostRegex) {
preg_match('#^.\^(.*)\$.[a-zA-Z]*$#', $hostRegex, $rx);
$state->vars = [];
$hostRegex = '(?i:'.preg_replace_callback('#\?P<([^>]++)>#', $state->getVars, $rx[1]).')\.';
$state->hostVars = $state->vars;
} else {
$hostRegex = '(?:(?:[^./]*+\.)++)';
$state->hostVars = [];
}
$state->mark += \strlen($rx = ($prev ? ')' : '')."|{$hostRegex}(?");
$code .= "\n .".self::export($rx);
$state->regex .= $rx;
$prev = true;
}
$tree = new StaticPrefixCollection();
foreach ($routes->all() as $name => $route) {
preg_match('#^.\^(.*)\$.[a-zA-Z]*$#', $route->compile()->getRegex(), $rx);
$state->vars = [];
$regex = preg_replace_callback('#\?P<([^>]++)>#', $state->getVars, $rx[1]);
if ($hasTrailingSlash = '/' !== $regex && '/' === $regex[-1]) {
$regex = substr($regex, 0, -1);
}
$hasTrailingVar = (bool) preg_match('#\{\w+\}/?$#', $route->getPath());
$tree->addRoute($regex, [$name, $regex, $state->vars, $route, $hasTrailingSlash, $hasTrailingVar]);
}
$code .= $this->compileStaticPrefixCollection($tree, $state, 0, $conditions);
}
if ($matchHost) {
$code .= "\n .')'";
$state->regex .= ')';
}
$rx = ")/?$}{$modifiers}";
$code .= "\n .'{$rx}',";
$state->regex .= $rx;
$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 str_contains($message, $this->signalingException->getMessage()) ? $this->signalingException : new \ErrorException($message); });
try {
preg_match($state->regex, '');
} finally {
restore_error_handler();
}
$regexpList[$startingMark] = $state->regex;
}
$state->routes[$state->mark][] = [null, null, null, null, false, false, 0];
unset($state->getVars);
return [$regexpList, $state->routes, $code];
}
/**
* Compiles a regexp tree of subpatterns that matches nested same-prefix routes.
*
* @param \stdClass $state A simple state object that keeps track of the progress of the compilation,
* and gathers the generated switch's "case" and "default" statements
*/
private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \stdClass $state, int $prefixLen, array &$conditions): string
{
$code = '';
$prevRegex = null;
$routes = $tree->getRoutes();
foreach ($routes as $i => $route) {
if ($route instanceof StaticPrefixCollection) {
$prevRegex = null;
$prefix = substr($route->getPrefix(), $prefixLen);
$state->mark += \strlen($rx = "|{$prefix}(?");
$code .= "\n .".self::export($rx);
$state->regex .= $rx;
$code .= $this->indent($this->compileStaticPrefixCollection($route, $state, $prefixLen + \strlen($prefix), $conditions));
$code .= "\n .')'";
$state->regex .= ')';
++$state->markTail;
continue;
}
[$name, $regex, $vars, $route, $hasTrailingSlash, $hasTrailingVar] = $route;
$compiledRoute = $route->compile();
$vars = array_merge($state->hostVars, $vars);
if ($compiledRoute->getRegex() === $prevRegex) {
$state->routes[$state->mark][] = $this->compileRoute($route, $name, $vars, $hasTrailingSlash, $hasTrailingVar, $conditions);
continue;
}
$state->mark += 3 + $state->markTail + \strlen($regex) - $prefixLen;
$state->markTail = 2 + \strlen($state->mark);
$rx = sprintf('|%s(*:%s)', substr($regex, $prefixLen), $state->mark);
$code .= "\n .".self::export($rx);
$state->regex .= $rx;
$prevRegex = $compiledRoute->getRegex();
$state->routes[$state->mark] = [$this->compileRoute($route, $name, $vars, $hasTrailingSlash, $hasTrailingVar, $conditions)];
}
return $code;
}
/**
* Compiles a single Route to PHP code used to match it against the path info.
*/
private function compileRoute(Route $route, string $name, string|array|null $vars, bool $hasTrailingSlash, bool $hasTrailingVar, array &$conditions): array
{
$defaults = $route->getDefaults();
if (isset($defaults['_canonical_route'])) {
$name = $defaults['_canonical_route'];
unset($defaults['_canonical_route']);
}
if ($condition = $route->getCondition()) {
$condition = $this->getExpressionLanguage()->compile($condition, ['context', 'request']);
$condition = $conditions[$condition] ?? $conditions[$condition] = (str_contains($condition, '$request') ? 1 : -1) * \count($conditions);
} else {
$condition = null;
}
return [
['_route' => $name] + $defaults,
$vars,
array_flip($route->getMethods()) ?: null,
array_flip($route->getSchemes()) ?: null,
$hasTrailingSlash,
$hasTrailingVar,
$condition,
];
}
private function getExpressionLanguage(): ExpressionLanguage
{
if (!isset($this->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);
}
return $this->expressionLanguage;
}
private function indent(string $code, int $level = 1): string
{
return preg_replace('/^./m', str_repeat(' ', $level).'$0', $code);
}
/**
* @internal
*/
public static function export(mixed $value): string
{
if (null === $value) {
return 'null';
}
if (!\is_array($value)) {
if (\is_object($value)) {
throw new \InvalidArgumentException('Symfony\Component\Routing\Route cannot contain objects.');
}
return str_replace("\n", '\'."\n".\'', var_export($value, true));
}
if (!$value) {
return '[]';
}
$i = 0;
$export = '[';
foreach ($value as $k => $v) {
if ($i === $k) {
++$i;
} else {
$export .= self::export($k).' => ';
if (\is_int($k) && $i < $k) {
$i = 1 + $k;
}
}
$export .= self::export($v).', ';
}
return substr_replace($export, ']', -2);
}
}

View File

@@ -0,0 +1,191 @@
<?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;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
use Symfony\Component\Routing\RequestContext;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*
* @property RequestContext $context
*/
trait CompiledUrlMatcherTrait
{
private bool $matchHost = false;
private array $staticRoutes = [];
private array $regexpList = [];
private array $dynamicRoutes = [];
/**
* @var callable|null
*/
private $checkCondition;
public function match(string $pathinfo): array
{
$allow = $allowSchemes = [];
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $ret;
}
if ($allow) {
throw new MethodNotAllowedException(array_keys($allow));
}
if (!$this instanceof RedirectableUrlMatcherInterface) {
throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
}
if (!\in_array($this->context->getMethod(), ['HEAD', 'GET'], true)) {
// no-op
} elseif ($allowSchemes) {
redirect_scheme:
$scheme = $this->context->getScheme();
$this->context->setScheme(key($allowSchemes));
try {
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route'], $this->context->getScheme()) + $ret;
}
} finally {
$this->context->setScheme($scheme);
}
} elseif ('/' !== $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/') {
$pathinfo = $trimmedPathinfo === $pathinfo ? $pathinfo.'/' : $trimmedPathinfo;
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
if ($allowSchemes) {
goto redirect_scheme;
}
}
throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
}
private function doMatch(string $pathinfo, array &$allow = [], array &$allowSchemes = []): array
{
$allow = $allowSchemes = [];
$pathinfo = rawurldecode($pathinfo) ?: '/';
$trimmedPathinfo = rtrim($pathinfo, '/') ?: '/';
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
if ($this->matchHost) {
$host = strtolower($context->getHost());
}
if ('HEAD' === $requestMethod) {
$canonicalMethod = 'GET';
}
$supportsRedirections = 'GET' === $canonicalMethod && $this instanceof RedirectableUrlMatcherInterface;
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)) {
continue;
}
if ('{' === $requiredHost[0] && $hostMatches) {
$hostMatches['_route'] = $ret['_route'];
$ret = $this->mergeDefaults($hostMatches, $ret);
}
}
if ('/' !== $pathinfo && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) {
return $allow = $allowSchemes = [];
}
continue;
}
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($hasRequiredScheme && $requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
continue;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
continue;
}
return $ret;
}
$matchedPathinfo = $this->matchHost ? $host.'.'.$pathinfo : $pathinfo;
foreach ($this->regexpList as $offset => $regex) {
while (preg_match($regex, $matchedPathinfo, $matches)) {
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;
}
if (!($this->checkCondition)($condition, $context, 0 < $condition ? $request ?? $request = $this->request ?: $this->createRequest($pathinfo) : null)) {
continue;
}
}
$hasTrailingVar = $trimmedPathinfo !== $pathinfo && $hasTrailingVar;
if ($hasTrailingVar && ($hasTrailingSlash || (null === $n = $matches[\count($vars)] ?? null) || '/' !== ($n[-1] ?? '/')) && preg_match($regex, $this->matchHost ? $host.'.'.$trimmedPathinfo : $trimmedPathinfo, $n) && $m === (int) $n['MARK']) {
if ($hasTrailingSlash) {
$matches = $n;
} else {
$hasTrailingVar = false;
}
}
if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) {
return $allow = $allowSchemes = [];
}
continue;
}
foreach ($vars as $i => $v) {
if (isset($matches[1 + $i])) {
$ret[$v] = $matches[1 + $i];
}
}
if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) {
$allowSchemes += $requiredSchemes;
continue;
}
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
continue;
}
return $ret;
}
$regex = substr_replace($regex, 'F', $m - $offset, 1 + \strlen($m));
$offset += \strlen($m);
}
}
if ('/' === $pathinfo && !$allow && !$allowSchemes) {
throw new NoConfigurationException();
}
return [];
}
}

View File

@@ -1,161 +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;
/**
* Collection of routes.
*
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
*
* @internal
*/
class DumperCollection implements \IteratorAggregate
{
/**
* @var DumperCollection|null
*/
private $parent;
/**
* @var DumperCollection[]|DumperRoute[]
*/
private $children = array();
/**
* @var array
*/
private $attributes = array();
/**
* Returns the children routes and collections.
*
* @return self[]|DumperRoute[]
*/
public function all()
{
return $this->children;
}
/**
* Adds a route or collection.
*
* @param DumperRoute|DumperCollection The route or collection
*/
public function add($child)
{
if ($child instanceof self) {
$child->setParent($this);
}
$this->children[] = $child;
}
/**
* Sets children.
*
* @param array $children The children
*/
public function setAll(array $children)
{
foreach ($children as $child) {
if ($child instanceof self) {
$child->setParent($this);
}
}
$this->children = $children;
}
/**
* Returns an iterator over the children.
*
* @return \Iterator|DumperCollection[]|DumperRoute[] The iterator
*/
public function getIterator()
{
return new \ArrayIterator($this->children);
}
/**
* Returns the root of the collection.
*
* @return self The root collection
*/
public function getRoot()
{
return (null !== $this->parent) ? $this->parent->getRoot() : $this;
}
/**
* Returns the parent collection.
*
* @return self|null The parent collection or null if the collection has no parent
*/
protected function getParent()
{
return $this->parent;
}
/**
* Sets the parent collection.
*
* @param DumperCollection $parent The parent collection
*/
protected function setParent(DumperCollection $parent)
{
$this->parent = $parent;
}
/**
* Returns true if the attribute is defined.
*
* @param string $name The attribute name
*
* @return bool true if the attribute is defined, false otherwise
*/
public function hasAttribute($name)
{
return array_key_exists($name, $this->attributes);
}
/**
* Returns an attribute by name.
*
* @param string $name The attribute name
* @param mixed $default Default value is the attribute doesn't exist
*
* @return mixed The attribute value
*/
public function getAttribute($name, $default = null)
{
return $this->hasAttribute($name) ? $this->attributes[$name] : $default;
}
/**
* Sets an attribute by name.
*
* @param string $name The attribute name
* @param mixed $value The attribute value
*/
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
}
/**
* Sets multiple attributes.
*
* @param array $attributes The attributes
*/
public function setAttributes($attributes)
{
$this->attributes = $attributes;
}
}

View File

@@ -1,66 +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;
use Symfony\Component\Routing\Route;
/**
* Container for a Route.
*
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
*
* @internal
*/
class DumperRoute
{
/**
* @var string
*/
private $name;
/**
* @var Route
*/
private $route;
/**
* Constructor.
*
* @param string $name The route name
* @param Route $route The route
*/
public function __construct($name, Route $route)
{
$this->name = $name;
$this->route = $route;
}
/**
* Returns the route name.
*
* @return string The route name
*/
public function getName()
{
return $this->name;
}
/**
* Returns the route.
*
* @return Route The route
*/
public function getRoute()
{
return $this->route;
}
}

View File

@@ -20,16 +20,8 @@ use Symfony\Component\Routing\RouteCollection;
*/
abstract class MatcherDumper implements MatcherDumperInterface
{
/**
* @var RouteCollection
*/
private $routes;
/**
* Constructor.
*
* @param RouteCollection $routes The RouteCollection to dump
*/
public function __construct(RouteCollection $routes)
{
$this->routes = $routes;
@@ -38,7 +30,7 @@ abstract class MatcherDumper implements MatcherDumperInterface
/**
* {@inheritdoc}
*/
public function getRoutes()
public function getRoutes(): RouteCollection
{
return $this->routes;
}

View File

@@ -23,17 +23,11 @@ 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
*/
public function dump(array $options = array());
public function dump(array $options = []): string;
/**
* Gets the routes to dump.
*
* @return RouteCollection A RouteCollection instance
*/
public function getRoutes();
public function getRoutes(): RouteCollection;
}

View File

@@ -1,431 +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;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
/**
* 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>
*/
class PhpMatcherDumper extends MatcherDumper
{
private $expressionLanguage;
/**
* @var ExpressionFunctionProviderInterface[]
*/
private $expressionLanguageProviders = array();
/**
* 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 = array())
{
$options = array_replace(array(
'class' => 'ProjectUrlMatcher',
'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
), $options);
// trailing slash support is only enabled if we know how to redirect the user
$interfaces = class_implements($options['base_class']);
$supportsRedirections = isset($interfaces['Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface']);
return <<<EOF
<?php
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;
/**
* {$options['class']}.
*
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class {$options['class']} extends {$options['base_class']}
{
/**
* Constructor.
*/
public function __construct(RequestContext \$context)
{
\$this->context = \$context;
}
{$this->generateMatchMethod($supportsRedirections)}
}
EOF;
}
public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
{
$this->expressionLanguageProviders[] = $provider;
}
/**
* Generates the code for the match method implementing UrlMatcherInterface.
*
* @param bool $supportsRedirections Whether redirections are supported by the base class
*
* @return string Match method as PHP code
*/
private function generateMatchMethod($supportsRedirections)
{
$code = rtrim($this->compileRoutes($this->getRoutes(), $supportsRedirections), "\n");
return <<<EOF
public function match(\$pathinfo)
{
\$allow = array();
\$pathinfo = rawurldecode(\$pathinfo);
\$trimmedPathinfo = rtrim(\$pathinfo, '/');
\$context = \$this->context;
\$request = \$this->request;
\$requestMethod = \$canonicalMethod = \$context->getMethod();
\$scheme = \$context->getScheme();
if ('HEAD' === \$requestMethod) {
\$canonicalMethod = 'GET';
}
$code
throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException();
}
EOF;
}
/**
* Generates PHP code to match a RouteCollection with all its routes.
*
* @param RouteCollection $routes A RouteCollection instance
* @param bool $supportsRedirections Whether redirections are supported by the base class
*
* @return string PHP code
*/
private function compileRoutes(RouteCollection $routes, $supportsRedirections)
{
$fetchedHost = false;
$groups = $this->groupRoutesByHostRegex($routes);
$code = '';
foreach ($groups as $collection) {
if (null !== $regex = $collection->getAttribute('host_regex')) {
if (!$fetchedHost) {
$code .= " \$host = \$context->getHost();\n\n";
$fetchedHost = true;
}
$code .= sprintf(" if (preg_match(%s, \$host, \$hostMatches)) {\n", var_export($regex, true));
}
$tree = $this->buildStaticPrefixCollection($collection);
$groupCode = $this->compileStaticPrefixRoutes($tree, $supportsRedirections);
if (null !== $regex) {
// apply extra indention at each line (except empty ones)
$groupCode = preg_replace('/^.{2,}$/m', ' $0', $groupCode);
$code .= $groupCode;
$code .= " }\n\n";
} else {
$code .= $groupCode;
}
}
return $code;
}
private function buildStaticPrefixCollection(DumperCollection $collection)
{
$prefixCollection = new StaticPrefixCollection();
foreach ($collection as $dumperRoute) {
$prefix = $dumperRoute->getRoute()->compile()->getStaticPrefix();
$prefixCollection->addRoute($prefix, $dumperRoute);
}
$prefixCollection->optimizeGroups();
return $prefixCollection;
}
/**
* Generates PHP code to match a tree of routes.
*
* @param StaticPrefixCollection $collection A StaticPrefixCollection instance
* @param bool $supportsRedirections Whether redirections are supported by the base class
* @param string $ifOrElseIf Either "if" or "elseif" to influence chaining.
*
* @return string PHP code
*/
private function compileStaticPrefixRoutes(StaticPrefixCollection $collection, $supportsRedirections, $ifOrElseIf = 'if')
{
$code = '';
$prefix = $collection->getPrefix();
if (!empty($prefix) && '/' !== $prefix) {
$code .= sprintf(" %s (0 === strpos(\$pathinfo, %s)) {\n", $ifOrElseIf, var_export($prefix, true));
}
$ifOrElseIf = 'if';
foreach ($collection->getItems() as $route) {
if ($route instanceof StaticPrefixCollection) {
$code .= $this->compileStaticPrefixRoutes($route, $supportsRedirections, $ifOrElseIf);
$ifOrElseIf = 'elseif';
} else {
$code .= $this->compileRoute($route[1]->getRoute(), $route[1]->getName(), $supportsRedirections, $prefix)."\n";
$ifOrElseIf = 'if';
}
}
if (!empty($prefix) && '/' !== $prefix) {
$code .= " }\n\n";
// apply extra indention at each line (except empty ones)
$code = preg_replace('/^.{2,}$/m', ' $0', $code);
}
return $code;
}
/**
* Compiles a single Route to PHP code used to match it against the path info.
*
* @param Route $route A Route instance
* @param string $name The name of the Route
* @param bool $supportsRedirections Whether redirections are supported by the base class
* @param string|null $parentPrefix The prefix of the parent collection used to optimize the code
*
* @return string PHP code
*
* @throws \LogicException
*/
private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null)
{
$code = '';
$compiledRoute = $route->compile();
$conditions = array();
$hasTrailingSlash = false;
$matches = false;
$hostMatches = false;
$methods = $route->getMethods();
$supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods) || in_array('GET', $methods));
$regex = $compiledRoute->getRegex();
if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#'.(substr($regex, -1) === 'u' ? 'u' : ''), $regex, $m)) {
if ($supportsTrailingSlash && substr($m['url'], -1) === '/') {
$conditions[] = sprintf('%s === $trimmedPathinfo', var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
$hasTrailingSlash = true;
} else {
$conditions[] = sprintf('%s === $pathinfo', var_export(str_replace('\\', '', $m['url']), true));
}
} else {
if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) {
$conditions[] = sprintf('0 === strpos($pathinfo, %s)', var_export($compiledRoute->getStaticPrefix(), true));
}
if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) {
$regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
$hasTrailingSlash = true;
}
$conditions[] = sprintf('preg_match(%s, $pathinfo, $matches)', var_export($regex, true));
$matches = true;
}
if ($compiledRoute->getHostVariables()) {
$hostMatches = true;
}
if ($route->getCondition()) {
$conditions[] = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request'));
}
$conditions = implode(' && ', $conditions);
$code .= <<<EOF
// $name
if ($conditions) {
EOF;
$gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
if ($methods) {
if (1 === count($methods)) {
if ($methods[0] === 'HEAD') {
$code .= <<<EOF
if ('HEAD' !== \$requestMethod) {
\$allow[] = 'HEAD';
goto $gotoname;
}
EOF;
} else {
$code .= <<<EOF
if ('$methods[0]' !== \$canonicalMethod) {
\$allow[] = '$methods[0]';
goto $gotoname;
}
EOF;
}
} else {
$methodVariable = 'requestMethod';
if (in_array('GET', $methods)) {
// Since we treat HEAD requests like GET requests we don't need to match it.
$methodVariable = 'canonicalMethod';
$methods = array_filter($methods, function ($method) { return 'HEAD' !== $method; });
}
if (1 === count($methods)) {
$code .= <<<EOF
if ('$methods[0]' !== \$$methodVariable) {
\$allow[] = '$methods[0]';
goto $gotoname;
}
EOF;
} else {
$methods = implode("', '", $methods);
$code .= <<<EOF
if (!in_array(\$$methodVariable, array('$methods'))) {
\$allow = array_merge(\$allow, array('$methods'));
goto $gotoname;
}
EOF;
}
}
}
if ($hasTrailingSlash) {
$code .= <<<EOF
if (substr(\$pathinfo, -1) !== '/') {
return \$this->redirect(\$pathinfo.'/', '$name');
}
EOF;
}
if ($schemes = $route->getSchemes()) {
if (!$supportsRedirections) {
throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
}
$schemes = str_replace("\n", '', var_export(array_flip($schemes), true));
$code .= <<<EOF
\$requiredSchemes = $schemes;
if (!isset(\$requiredSchemes[\$scheme])) {
return \$this->redirect(\$pathinfo, '$name', key(\$requiredSchemes));
}
EOF;
}
// optimize parameters array
if ($matches || $hostMatches) {
$vars = array();
if ($hostMatches) {
$vars[] = '$hostMatches';
}
if ($matches) {
$vars[] = '$matches';
}
$vars[] = "array('_route' => '$name')";
$code .= sprintf(
" return \$this->mergeDefaults(array_replace(%s), %s);\n",
implode(', ', $vars),
str_replace("\n", '', var_export($route->getDefaults(), true))
);
} elseif ($route->getDefaults()) {
$code .= sprintf(" return %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), array('_route' => $name)), true)));
} else {
$code .= sprintf(" return array('_route' => '%s');\n", $name);
}
$code .= " }\n";
if ($methods) {
$code .= " $gotoname:\n";
}
return $code;
}
/**
* Groups consecutive routes having the same host regex.
*
* The result is a collection of collections of routes having the same host regex.
*
* @param RouteCollection $routes A flat RouteCollection
*
* @return DumperCollection A collection with routes grouped by host regex in sub-collections
*/
private function groupRoutesByHostRegex(RouteCollection $routes)
{
$groups = new DumperCollection();
$currentGroup = new DumperCollection();
$currentGroup->setAttribute('host_regex', null);
$groups->add($currentGroup);
foreach ($routes as $name => $route) {
$hostRegex = $route->compile()->getHostRegex();
if ($currentGroup->getAttribute('host_regex') !== $hostRegex) {
$currentGroup = new DumperCollection();
$currentGroup->setAttribute('host_regex', $hostRegex);
$groups->add($currentGroup);
}
$currentGroup->add(new DumperRoute($name, $route));
}
return $groups;
}
private function getExpressionLanguage()
{
if (null === $this->expressionLanguage) {
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
}
$this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
}
return $this->expressionLanguage;
}
}

View File

@@ -11,228 +11,193 @@
namespace Symfony\Component\Routing\Matcher\Dumper;
use Symfony\Component\Routing\RouteCollection;
/**
* Prefix tree of routes preserving routes order.
*
* @author Frank de Jonge <info@frankdejonge.nl>
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class StaticPrefixCollection
{
/**
* @var string
*/
private $prefix;
private string $prefix;
/**
* @var array[]|StaticPrefixCollection[]
* @var string[]
*/
private $items = array();
private array $staticPrefixes = [];
/**
* @var int
* @var string[]
*/
private $matchStart = 0;
private array $prefixes = [];
public function __construct($prefix = '')
/**
* @var array[]|self[]
*/
private array $items = [];
public function __construct(string $prefix = '/')
{
$this->prefix = $prefix;
}
public function getPrefix()
public function getPrefix(): string
{
return $this->prefix;
}
/**
* @return mixed[]|StaticPrefixCollection[]
* @return array[]|self[]
*/
public function getItems()
public function getRoutes(): array
{
return $this->items;
}
/**
* Adds a route to a group.
*
* @param string $prefix
* @param mixed $route
*/
public function addRoute($prefix, $route)
public function addRoute(string $prefix, array|StaticPrefixCollection $route)
{
$prefix = '/' === $prefix ? $prefix : rtrim($prefix, '/');
$this->guardAgainstAddingNotAcceptedRoutes($prefix);
[$prefix, $staticPrefix] = $this->getCommonPrefix($prefix, $prefix);
if ($this->prefix === $prefix) {
// When a prefix is exactly the same as the base we move up the match start position.
// This is needed because otherwise routes that come afterwards have higher precedence
// than a possible regular expression, which goes against the input order sorting.
$this->items[] = array($prefix, $route);
$this->matchStart = count($this->items);
for ($i = \count($this->items) - 1; 0 <= $i; --$i) {
$item = $this->items[$i];
return;
}
[$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
if ($this->prefix !== $staticPrefix && $this->prefix !== $this->staticPrefixes[$i]) {
// the new route and the previous one have exclusive static prefixes
continue;
}
if ($this->prefix === $staticPrefix && $this->prefix === $this->staticPrefixes[$i]) {
// the new route and the previous one have no static prefix
break;
}
if ($this->prefixes[$i] !== $this->staticPrefixes[$i] && $this->prefix === $this->staticPrefixes[$i]) {
// the previous route is non-static and has no static prefix
break;
}
if ($prefix !== $staticPrefix && $this->prefix === $staticPrefix) {
// the new route is non-static and has no static prefix
break;
}
foreach ($this->items as $i => $item) {
if ($i < $this->matchStart) {
continue;
}
if ($item instanceof self && $item->accepts($prefix)) {
if ($item instanceof self && $this->prefixes[$i] === $commonPrefix) {
// the new route is a child of a previous one, let's nest it
$item->addRoute($prefix, $route);
} else {
// the new route and a previous one have a common prefix, let's merge them
$child = new self($commonPrefix);
[$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];
return;
$this->staticPrefixes[$i] = $commonStaticPrefix;
$this->prefixes[$i] = $commonPrefix;
$this->items[$i] = $child;
}
$group = $this->groupWithItem($item, $prefix, $route);
if ($group instanceof self) {
$this->items[$i] = $group;
return;
}
return;
}
// No optimised case was found, in this case we simple add the route for possible
// grouping when new routes are added.
$this->items[] = array($prefix, $route);
$this->staticPrefixes[] = $staticPrefix;
$this->prefixes[] = $prefix;
$this->items[] = $route;
}
/**
* Tries to combine a route with another route or group.
*
* @param StaticPrefixCollection|array $item
* @param string $prefix
* @param mixed $route
*
* @return null|StaticPrefixCollection
* Linearizes back a set of nested routes into a collection.
*/
private function groupWithItem($item, $prefix, $route)
public function populateCollection(RouteCollection $routes): RouteCollection
{
$itemPrefix = $item instanceof self ? $item->prefix : $item[0];
$commonPrefix = $this->detectCommonPrefix($prefix, $itemPrefix);
if (!$commonPrefix) {
return;
}
$child = new self($commonPrefix);
if ($item instanceof self) {
$child->items = array($item);
} else {
$child->addRoute($item[0], $item[1]);
}
$child->addRoute($prefix, $route);
return $child;
}
/**
* Checks whether a prefix can be contained within the group.
*
* @param string $prefix
*
* @return bool Whether a prefix could belong in a given group
*/
private function accepts($prefix)
{
return '' === $this->prefix || strpos($prefix, $this->prefix) === 0;
}
/**
* Detects whether there's a common prefix relative to the group prefix and returns it.
*
* @param string $prefix
* @param string $anotherPrefix
*
* @return false|string A common prefix, longer than the base/group prefix, or false when none available
*/
private function detectCommonPrefix($prefix, $anotherPrefix)
{
$baseLength = strlen($this->prefix);
$commonLength = $baseLength;
$end = min(strlen($prefix), strlen($anotherPrefix));
for ($i = $baseLength; $i <= $end; ++$i) {
if (substr($prefix, 0, $i) !== substr($anotherPrefix, 0, $i)) {
break;
foreach ($this->items as $route) {
if ($route instanceof self) {
$route->populateCollection($routes);
} else {
$routes->add(...$route);
}
$commonLength = $i;
}
$commonPrefix = rtrim(substr($prefix, 0, $commonLength), '/');
if (strlen($commonPrefix) > $baseLength) {
return $commonPrefix;
}
return false;
return $routes;
}
/**
* Optimizes the tree by inlining items from groups with less than 3 items.
* Gets the full and static common prefixes between two route patterns.
*
* The static prefix stops at last at the first opening bracket.
*/
public function optimizeGroups()
private function getCommonPrefix(string $prefix, string $anotherPrefix): array
{
$index = -1;
$baseLength = \strlen($this->prefix);
$end = min(\strlen($prefix), \strlen($anotherPrefix));
$staticLength = null;
set_error_handler([__CLASS__, 'handleError']);
while (isset($this->items[++$index])) {
$item = $this->items[$index];
if ($item instanceof self) {
$item->optimizeGroups();
// When a group contains only two items there's no reason to optimize because at minimum
// the amount of prefix check is 2. In this case inline the group.
if ($item->shouldBeInlined()) {
array_splice($this->items, $index, 1, $item->items);
// Lower index to pass through the same index again after optimizing.
// The first item of the replacements might be a group needing optimization.
--$index;
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 (0 < $n) {
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();
}
if ($i < $end && 0b10 === (\ord($prefix[$i]) >> 6) && preg_match('//u', $prefix.' '.$anotherPrefix)) {
do {
// Prevent cutting in the middle of an UTF-8 characters
--$i;
} while (0b10 === (\ord($prefix[$i]) >> 6));
}
return [substr($prefix, 0, $i), substr($prefix, 0, $staticLength ?? $i)];
}
private function shouldBeInlined()
public static function handleError(int $type, string $msg)
{
if (count($this->items) >= 3) {
return false;
}
foreach ($this->items as $item) {
if ($item instanceof self) {
return true;
}
}
foreach ($this->items as $item) {
if (is_array($item) && $item[0] === $this->prefix) {
return false;
}
}
return true;
}
/**
* Guards against adding incompatible prefixes in a group.
*
* @param string $prefix
*
* @throws \LogicException When a prefix does not belong in a group.
*/
private function guardAgainstAddingNotAcceptedRoutes($prefix)
{
if (!$this->accepts($prefix)) {
$message = sprintf('Could not add route with prefix %s to collection with prefix %s', $prefix, $this->prefix);
throw new \LogicException($message);
}
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(): array
{
$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

@@ -11,8 +11,8 @@
namespace Symfony\Component\Routing\Matcher;
use Symfony\Component\Routing\Exception\ExceptionInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Route;
/**
* @author Fabien Potencier <fabien@symfony.com>
@@ -22,44 +22,43 @@ abstract class RedirectableUrlMatcher extends UrlMatcher implements Redirectable
/**
* {@inheritdoc}
*/
public function match($pathinfo)
public function match(string $pathinfo): array
{
try {
$parameters = parent::match($pathinfo);
return parent::match($pathinfo);
} catch (ResourceNotFoundException $e) {
if ('/' === substr($pathinfo, -1) || !in_array($this->context->getMethod(), array('HEAD', 'GET'))) {
if (!\in_array($this->context->getMethod(), ['HEAD', 'GET'], true)) {
throw $e;
}
try {
parent::match($pathinfo.'/');
if ($this->allowSchemes) {
redirect_scheme:
$scheme = $this->context->getScheme();
$this->context->setScheme(current($this->allowSchemes));
try {
$ret = parent::match($pathinfo);
return $this->redirect($pathinfo.'/', null);
} catch (ResourceNotFoundException $e2) {
return $this->redirect($pathinfo, $ret['_route'] ?? null, $this->context->getScheme()) + $ret;
} catch (ExceptionInterface $e2) {
throw $e;
} finally {
$this->context->setScheme($scheme);
}
} elseif ('/' === $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/') {
throw $e;
} else {
try {
$pathinfo = $trimmedPathinfo === $pathinfo ? $pathinfo.'/' : $trimmedPathinfo;
$ret = parent::match($pathinfo);
return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret;
} catch (ExceptionInterface $e2) {
if ($this->allowSchemes) {
goto redirect_scheme;
}
throw $e;
}
}
}
return $parameters;
}
/**
* {@inheritdoc}
*/
protected function handleRouteRequirements($pathinfo, $name, Route $route)
{
// expression condition
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)))) {
return array(self::REQUIREMENT_MISMATCH, null);
}
// check HTTP scheme requirement
$scheme = $this->context->getScheme();
$schemes = $route->getSchemes();
if ($schemes && !$route->hasScheme($scheme)) {
return array(self::ROUTE_MATCH, $this->redirect($pathinfo, $name, current($schemes)));
}
return array(self::REQUIREMENT_MATCH, null);
}
}

View File

@@ -19,13 +19,11 @@ 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
*/
public function redirect($path, $route, $scheme = null);
public function redirect(string $path, string $route, string $scheme = null): array;
}

View File

@@ -12,8 +12,9 @@
namespace Symfony\Component\Routing\Matcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* RequestMatcherInterface is the interface that all request matcher classes must implement.
@@ -25,15 +26,12 @@ 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.
*
* @param Request $request The request to match
*
* @return array An array of parameters
*
* @throws NoConfigurationException If no routing configuration could be found
* @throws ResourceNotFoundException If no matching resource could be found
* @throws MethodNotAllowedException If a matching resource was found but the request method is not allowed
*/
public function matchRequest(Request $request);
public function matchRequest(Request $request): array;
}

View File

@@ -23,15 +23,15 @@ 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 = array();
$this->traces = [];
try {
$this->match($pathinfo);
@@ -50,14 +50,34 @@ class TraceableUrlMatcher extends UrlMatcher
return $traces;
}
protected function matchCollection($pathinfo, RouteCollection $routes)
protected function matchCollection(string $pathinfo, RouteCollection $routes): array
{
// 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(), array(), $route->getOptions());
$r = new Route($route->getPath(), $route->getDefaults(), [], $route->getOptions());
$cr = $r->compile();
if (!preg_match($cr->getRegex(), $pathinfo)) {
$this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route);
@@ -66,10 +86,10 @@ class TraceableUrlMatcher extends UrlMatcher
}
foreach ($route->getRequirements() as $n => $regex) {
$r = new Route($route->getPath(), $route->getDefaults(), array($n => $regex), $route->getOptions());
$r = new Route($route->getPath(), $route->getDefaults(), [$n => $regex], $route->getOptions());
$cr = $r->compile();
if (in_array($n, $cr->getVariables()) && !preg_match($cr->getRegex(), $pathinfo)) {
if (\in_array($n, $cr->getVariables()) && !preg_match($cr->getRegex(), $pathinfo)) {
$this->addTrace(sprintf('Requirement for "%s" does not match (%s)', $n, $regex), self::ROUTE_ALMOST_MATCHES, $name, $route);
continue 2;
@@ -79,63 +99,66 @@ class TraceableUrlMatcher extends UrlMatcher
continue;
}
// check host requirement
$hostMatches = array();
$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, array('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[] = array(
$this->traces[] = [
'log' => $log,
'name' => $name,
'level' => $level,
'path' => null !== $route ? $route->getPath() : null,
);
];
}
}

View File

@@ -11,14 +11,15 @@
namespace Symfony\Component\Routing\Matcher;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Component\Routing\RouteCollection;
/**
* UrlMatcher matches URL based on a set of routes.
@@ -27,39 +28,34 @@ use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
*/
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
*/
/** @var RequestContext */
protected $context;
/**
* @var array
* Collects HTTP methods that would be allowed for the request.
*/
protected $allow = array();
protected $allow = [];
/**
* @var RouteCollection
* Collects URI schemes that would be allowed for the request.
*
* @internal
*/
protected $routes;
protected array $allowSchemes = [];
protected $routes;
protected $request;
protected $expressionLanguage;
/**
* @var ExpressionFunctionProviderInterface[]
*/
protected $expressionLanguageProviders = array();
protected $expressionLanguageProviders = [];
/**
* Constructor.
*
* @param RouteCollection $routes A RouteCollection instance
* @param RequestContext $context The context
*/
public function __construct(RouteCollection $routes, RequestContext $context)
{
$this->routes = $routes;
@@ -77,7 +73,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
/**
* {@inheritdoc}
*/
public function getContext()
public function getContext(): RequestContext
{
return $this->context;
}
@@ -85,23 +81,25 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
/**
* {@inheritdoc}
*/
public function match($pathinfo)
public function match(string $pathinfo): array
{
$this->allow = array();
$this->allow = $this->allowSchemes = [];
if ($ret = $this->matchCollection(rawurldecode($pathinfo), $this->routes)) {
if ($ret = $this->matchCollection(rawurldecode($pathinfo) ?: '/', $this->routes)) {
return $ret;
}
throw 0 < count($this->allow)
? new MethodNotAllowedException(array_unique($this->allow))
: new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
if ('/' === $pathinfo && !$this->allow && !$this->allowSchemes) {
throw new NoConfigurationException();
}
throw 0 < \count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
}
/**
* {@inheritdoc}
*/
public function matchRequest(Request $request)
public function matchRequest(Request $request): array
{
$this->request = $request;
@@ -120,59 +118,82 @@ 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
*
* @return array An array of parameters
* @param string $pathinfo The path info to be parsed
*
* @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): array
{
// 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();
// check the static prefix of the URL first. Only use the more expensive preg_match when it matches
if ('' !== $compiledRoute->getStaticPrefix() && 0 !== strpos($pathinfo, $compiledRoute->getStaticPrefix())) {
if ('' !== $staticPrefix && !str_starts_with($trimmedPathinfo, $staticPrefix)) {
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)) {
continue;
}
if (!preg_match($compiledRoute->getRegex(), $pathinfo, $matches)) {
continue;
$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 = array();
$hostMatches = [];
if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
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';
}
if (!in_array($method, $requiredMethods)) {
$this->allow = array_merge($this->allow, $requiredMethods);
continue;
}
}
$status = $this->handleRouteRequirements($pathinfo, $name, $route);
if (self::ROUTE_MATCH === $status[0]) {
return $status[1];
}
if (self::REQUIREMENT_MISMATCH === $status[0]) {
continue;
}
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) {
return $this->allow = $this->allowSchemes = [];
}
continue;
}
if ($route->getSchemes() && !$route->hasScheme($this->context->getScheme())) {
$this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes());
continue;
}
if ($requiredMethods && !\in_array($method, $requiredMethods)) {
$this->allow = array_merge($this->allow, $requiredMethods);
continue;
}
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, $status[1] ?? []));
}
return [];
}
/**
@@ -181,55 +202,41 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
* As this method requires the Route object, it is not available
* 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
*/
protected function getAttributes(Route $route, $name, array $attributes)
protected function getAttributes(Route $route, string $name, array $attributes): array
{
$defaults = $route->getDefaults();
if (isset($defaults['_canonical_route'])) {
$name = $defaults['_canonical_route'];
unset($defaults['_canonical_route']);
}
$attributes['_route'] = $name;
return $this->mergeDefaults($attributes, $route->getDefaults());
return $this->mergeDefaults($attributes, $defaults);
}
/**
* 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): array
{
// expression condition
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)))) {
return array(self::REQUIREMENT_MISMATCH, null);
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) {
return [self::REQUIREMENT_MISMATCH, null];
}
// check HTTP scheme requirement
$scheme = $this->context->getScheme();
$status = $route->getSchemes() && !$route->hasScheme($scheme) ? self::REQUIREMENT_MISMATCH : self::REQUIREMENT_MATCH;
return array($status, null);
return [self::REQUIREMENT_MATCH, null];
}
/**
* Get merged default parameters.
*
* @param array $params The parameters
* @param array $defaults The defaults
*
* @return array Merged default parameters
*/
protected function mergeDefaults($params, $defaults)
protected function mergeDefaults(array $params, array $defaults): array
{
foreach ($params as $key => $value) {
if (!is_int($key)) {
if (!\is_int($key) && null !== $value) {
$defaults[$key] = $value;
}
}
@@ -240,8 +247,8 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
protected function getExpressionLanguage()
{
if (null === $this->expressionLanguage) {
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
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);
}
@@ -252,15 +259,15 @@ 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;
}
return Request::create($this->context->getScheme().'://'.$this->context->getHost().$this->context->getBaseUrl().$pathinfo, $this->context->getMethod(), $this->context->getParameters(), array(), array(), array(
return Request::create($this->context->getScheme().'://'.$this->context->getHost().$this->context->getBaseUrl().$pathinfo, $this->context->getMethod(), $this->context->getParameters(), [], [], [
'SCRIPT_FILENAME' => $this->context->getBaseUrl(),
'SCRIPT_NAME' => $this->context->getBaseUrl(),
));
]);
}
}

View File

@@ -11,9 +11,10 @@
namespace Symfony\Component\Routing\Matcher;
use Symfony\Component\Routing\RequestContextAwareInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContextAwareInterface;
/**
* UrlMatcherInterface is the interface that all URL matcher classes must implement.
@@ -25,15 +26,14 @@ 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
*
* @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): array;
}

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

@@ -23,33 +23,17 @@ use Symfony\Component\HttpFoundation\Request;
*/
class RequestContext
{
private $baseUrl;
private $pathInfo;
private $method;
private $host;
private $scheme;
private $httpPort;
private $httpsPort;
private $queryString;
private string $baseUrl;
private string $pathInfo;
private string $method;
private string $host;
private string $scheme;
private int $httpPort;
private int $httpsPort;
private string $queryString;
private array $parameters = [];
/**
* @var array
*/
private $parameters = array();
/**
* Constructor.
*
* @param string $baseUrl The base URL
* @param string $method The HTTP method
* @param string $host The HTTP host name
* @param string $scheme The HTTP scheme
* @param int $httpPort The HTTP port
* @param int $httpsPort The HTTPS port
* @param string $path The path
* @param string $queryString The query string
*/
public function __construct($baseUrl = '', $method = 'GET', $host = 'localhost', $scheme = 'http', $httpPort = 80, $httpsPort = 443, $path = '/', $queryString = '')
public function __construct(string $baseUrl = '', string $method = 'GET', string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443, string $path = '/', string $queryString = '')
{
$this->setBaseUrl($baseUrl);
$this->setMethod($method);
@@ -61,22 +45,37 @@ 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.
*
* @param Request $request A Request instance
*
* @return $this
*/
public function fromRequest(Request $request)
public function fromRequest(Request $request): static
{
$this->setBaseUrl($request->getBaseUrl());
$this->setPathInfo($request->getPathInfo());
$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;
@@ -84,10 +83,8 @@ class RequestContext
/**
* Gets the base URL.
*
* @return string The base URL
*/
public function getBaseUrl()
public function getBaseUrl(): string
{
return $this->baseUrl;
}
@@ -95,23 +92,19 @@ class RequestContext
/**
* Sets the base URL.
*
* @param string $baseUrl The base URL
*
* @return $this
*/
public function setBaseUrl($baseUrl)
public function setBaseUrl(string $baseUrl): static
{
$this->baseUrl = $baseUrl;
$this->baseUrl = rtrim($baseUrl, '/');
return $this;
}
/**
* Gets the path info.
*
* @return string The path info
*/
public function getPathInfo()
public function getPathInfo(): string
{
return $this->pathInfo;
}
@@ -119,11 +112,9 @@ class RequestContext
/**
* Sets the path info.
*
* @param string $pathInfo The path info
*
* @return $this
*/
public function setPathInfo($pathInfo)
public function setPathInfo(string $pathInfo): static
{
$this->pathInfo = $pathInfo;
@@ -134,10 +125,8 @@ class RequestContext
* Gets the HTTP method.
*
* The method is always an uppercased string.
*
* @return string The HTTP method
*/
public function getMethod()
public function getMethod(): string
{
return $this->method;
}
@@ -145,11 +134,9 @@ class RequestContext
/**
* Sets the HTTP method.
*
* @param string $method The HTTP method
*
* @return $this
*/
public function setMethod($method)
public function setMethod(string $method): static
{
$this->method = strtoupper($method);
@@ -160,10 +147,8 @@ class RequestContext
* Gets the HTTP host.
*
* The host is always lowercased because it must be treated case-insensitive.
*
* @return string The HTTP host
*/
public function getHost()
public function getHost(): string
{
return $this->host;
}
@@ -171,11 +156,9 @@ class RequestContext
/**
* Sets the HTTP host.
*
* @param string $host The HTTP host
*
* @return $this
*/
public function setHost($host)
public function setHost(string $host): static
{
$this->host = strtolower($host);
@@ -184,10 +167,8 @@ class RequestContext
/**
* Gets the HTTP scheme.
*
* @return string The HTTP scheme
*/
public function getScheme()
public function getScheme(): string
{
return $this->scheme;
}
@@ -195,11 +176,9 @@ class RequestContext
/**
* Sets the HTTP scheme.
*
* @param string $scheme The HTTP scheme
*
* @return $this
*/
public function setScheme($scheme)
public function setScheme(string $scheme): static
{
$this->scheme = strtolower($scheme);
@@ -208,10 +187,8 @@ class RequestContext
/**
* Gets the HTTP port.
*
* @return int The HTTP port
*/
public function getHttpPort()
public function getHttpPort(): int
{
return $this->httpPort;
}
@@ -219,23 +196,19 @@ class RequestContext
/**
* Sets the HTTP port.
*
* @param int $httpPort The HTTP port
*
* @return $this
*/
public function setHttpPort($httpPort)
public function setHttpPort(int $httpPort): static
{
$this->httpPort = (int) $httpPort;
$this->httpPort = $httpPort;
return $this;
}
/**
* Gets the HTTPS port.
*
* @return int The HTTPS port
*/
public function getHttpsPort()
public function getHttpsPort(): int
{
return $this->httpsPort;
}
@@ -243,23 +216,19 @@ class RequestContext
/**
* Sets the HTTPS port.
*
* @param int $httpsPort The HTTPS port
*
* @return $this
*/
public function setHttpsPort($httpsPort)
public function setHttpsPort(int $httpsPort): static
{
$this->httpsPort = (int) $httpsPort;
$this->httpsPort = $httpsPort;
return $this;
}
/**
* Gets the query string.
*
* @return string The query string without the "?"
* Gets the query string without the "?".
*/
public function getQueryString()
public function getQueryString(): string
{
return $this->queryString;
}
@@ -267,11 +236,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): static
{
// string cast to be fault-tolerant, accepting null
$this->queryString = (string) $queryString;
@@ -281,10 +248,8 @@ class RequestContext
/**
* Returns the parameters.
*
* @return array The parameters
*/
public function getParameters()
public function getParameters(): array
{
return $this->parameters;
}
@@ -296,7 +261,7 @@ class RequestContext
*
* @return $this
*/
public function setParameters(array $parameters)
public function setParameters(array $parameters): static
{
$this->parameters = $parameters;
@@ -305,40 +270,34 @@ class RequestContext
/**
* Gets a parameter value.
*
* @param string $name A parameter name
*
* @return mixed The parameter value or null if nonexistent
*/
public function getParameter($name)
public function getParameter(string $name): mixed
{
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
*/
public function hasParameter($name)
public function hasParameter(string $name): bool
{
return array_key_exists($name, $this->parameters);
return \array_key_exists($name, $this->parameters);
}
/**
* Sets a parameter value.
*
* @param string $name A parameter name
* @param mixed $parameter The parameter value
*
* @return $this
*/
public function setParameter($name, $parameter)
public function setParameter(string $name, mixed $parameter): static
{
$this->parameters[$name] = $parameter;
return $this;
}
public function isSecure(): bool
{
return 'https' === $this->scheme;
}
}

View File

@@ -15,15 +15,11 @@ interface RequestContextAwareInterface
{
/**
* Sets the request context.
*
* @param RequestContext $context The context
*/
public function setContext(RequestContext $context);
/**
* Gets the request context.
*
* @return RequestContext The context
*/
public function getContext();
public function getContext(): RequestContext;
}

View File

@@ -19,50 +19,15 @@ namespace Symfony\Component\Routing;
*/
class Route implements \Serializable
{
/**
* @var string
*/
private $path = '/';
/**
* @var string
*/
private $host = '';
/**
* @var array
*/
private $schemes = array();
/**
* @var array
*/
private $methods = array();
/**
* @var array
*/
private $defaults = array();
/**
* @var array
*/
private $requirements = array();
/**
* @var array
*/
private $options = array();
/**
* @var null|CompiledRoute
*/
private $compiled;
/**
* @var string
*/
private $condition = '';
private string $path = '/';
private string $host = '';
private array $schemes = [];
private array $methods = [];
private array $defaults = [];
private array $requirements = [];
private array $options = [];
private string $condition = '';
private $compiled = null;
/**
* Constructor.
@@ -72,20 +37,20 @@ class Route implements \Serializable
* * compiler_class: A class name able to compile this route instance (RouteCompiler by default)
* * utf8: Whether UTF-8 matching is enforced ot not
*
* @param string $path The path pattern to match
* @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|array $schemes A required URI scheme or an array of restricted schemes
* @param string|array $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 $path The path pattern to match
* @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|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|null $condition A condition that should evaluate to true for the route to match
*/
public function __construct($path, array $defaults = array(), array $requirements = array(), array $options = array(), $host = '', $schemes = array(), $methods = array(), $condition = '')
public function __construct(string $path, array $defaults = [], array $requirements = [], array $options = [], ?string $host = '', string|array $schemes = [], string|array $methods = [], ?string $condition = '')
{
$this->setPath($path);
$this->setDefaults($defaults);
$this->setRequirements($requirements);
$this->addDefaults($defaults);
$this->addRequirements($requirements);
$this->setOptions($options);
$this->setHost($host);
$this->setSchemes($schemes);
@@ -93,12 +58,9 @@ class Route implements \Serializable
$this->setCondition($condition);
}
/**
* {@inheritdoc}
*/
public function serialize()
public function __serialize(): array
{
return serialize(array(
return [
'path' => $this->path,
'host' => $this->host,
'defaults' => $this->defaults,
@@ -108,15 +70,19 @@ class Route implements \Serializable
'methods' => $this->methods,
'condition' => $this->condition,
'compiled' => $this->compiled,
));
];
}
/**
* {@inheritdoc}
* @internal
*/
public function unserialize($serialized)
final public function serialize(): string
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __unserialize(array $data): void
{
$data = unserialize($serialized);
$this->path = $data['path'];
$this->host = $data['host'];
$this->defaults = $data['defaults'];
@@ -134,26 +100,25 @@ class Route implements \Serializable
}
/**
* Returns the pattern for the path.
*
* @return string The path pattern
* @internal
*/
public function getPath()
final public function unserialize(string $serialized)
{
$this->__unserialize(unserialize($serialized));
}
public function getPath(): string
{
return $this->path;
}
/**
* 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): static
{
$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'.
$this->path = '/'.ltrim(trim($pattern), '/');
@@ -162,28 +127,17 @@ class Route implements \Serializable
return $this;
}
/**
* Returns the pattern for the host.
*
* @return string The host pattern
*/
public function getHost()
public function getHost(): string
{
return $this->host;
}
/**
* 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): static
{
$this->host = (string) $pattern;
$this->host = $this->extractInlineDefaultsAndRequirements((string) $pattern);
$this->compiled = null;
return $this;
@@ -193,9 +147,9 @@ class Route implements \Serializable
* Returns the lowercased schemes this route is restricted to.
* So an empty array means that any scheme is allowed.
*
* @return array The schemes
* @return string[]
*/
public function getSchemes()
public function getSchemes(): array
{
return $this->schemes;
}
@@ -204,13 +158,11 @@ 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|array $schemes The scheme or an array of schemes
* @param string|string[] $schemes The scheme or an array of schemes
*
* @return $this
*/
public function setSchemes($schemes)
public function setSchemes(string|array $schemes): static
{
$this->schemes = array_map('strtolower', (array) $schemes);
$this->compiled = null;
@@ -220,23 +172,19 @@ 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
*/
public function hasScheme($scheme)
public function hasScheme(string $scheme): bool
{
return in_array(strtolower($scheme), $this->schemes, true);
return \in_array(strtolower($scheme), $this->schemes, true);
}
/**
* Returns the uppercased HTTP methods this route is restricted to.
* So an empty array means that any method is allowed.
*
* @return array The methods
* @return string[]
*/
public function getMethods()
public function getMethods(): array
{
return $this->methods;
}
@@ -245,13 +193,11 @@ 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|array $methods The method or an array of methods
* @param string|string[] $methods The method or an array of methods
*
* @return $this
*/
public function setMethods($methods)
public function setMethods(string|array $methods): static
{
$this->methods = array_map('strtoupper', (array) $methods);
$this->compiled = null;
@@ -259,44 +205,27 @@ class Route implements \Serializable
return $this;
}
/**
* Returns the options.
*
* @return array The options
*/
public function getOptions()
public function getOptions(): array
{
return $this->options;
}
/**
* Sets the options.
*
* This method implements a fluent interface.
*
* @param array $options The options
*
* @return $this
*/
public function setOptions(array $options)
public function setOptions(array $options): static
{
$this->options = array(
$this->options = [
'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler',
);
];
return $this->addOptions($options);
}
/**
* Adds options.
*
* This method implements a fluent interface.
*
* @param array $options The options
*
* @return $this
*/
public function addOptions(array $options)
public function addOptions(array $options): static
{
foreach ($options as $name => $option) {
$this->options[$name] = $option;
@@ -309,14 +238,9 @@ 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
*
* @return $this
*/
public function setOption($name, $value)
public function setOption(string $name, mixed $value): static
{
$this->options[$name] = $value;
$this->compiled = null;
@@ -325,66 +249,42 @@ class Route implements \Serializable
}
/**
* Get an option value.
*
* @param string $name An option name
*
* @return mixed The option value or null when not given
* Returns the option value or null when not found.
*/
public function getOption($name)
public function getOption(string $name): mixed
{
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
*/
public function hasOption($name)
public function hasOption(string $name): bool
{
return array_key_exists($name, $this->options);
return \array_key_exists($name, $this->options);
}
/**
* Returns the defaults.
*
* @return array The defaults
*/
public function getDefaults()
public function getDefaults(): array
{
return $this->defaults;
}
/**
* Sets the defaults.
*
* This method implements a fluent interface.
*
* @param array $defaults The defaults
*
* @return $this
*/
public function setDefaults(array $defaults)
public function setDefaults(array $defaults): static
{
$this->defaults = array();
$this->defaults = [];
return $this->addDefaults($defaults);
}
/**
* Adds defaults.
*
* This method implements a fluent interface.
*
* @param array $defaults The defaults
*
* @return $this
*/
public function addDefaults(array $defaults)
public function addDefaults(array $defaults): static
{
if (isset($defaults['_locale']) && $this->isLocalized()) {
unset($defaults['_locale']);
}
foreach ($defaults as $name => $default) {
$this->defaults[$name] = $default;
}
@@ -393,83 +293,55 @@ class Route implements \Serializable
return $this;
}
/**
* Gets a default value.
*
* @param string $name A variable name
*
* @return mixed The default value or null when not given
*/
public function getDefault($name)
public function getDefault(string $name): mixed
{
return isset($this->defaults[$name]) ? $this->defaults[$name] : null;
return $this->defaults[$name] ?? null;
}
public function hasDefault(string $name): bool
{
return \array_key_exists($name, $this->defaults);
}
/**
* 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
*/
public function hasDefault($name)
{
return array_key_exists($name, $this->defaults);
}
/**
* Sets a default value.
*
* @param string $name A variable name
* @param mixed $default The default value
*
* @return $this
*/
public function setDefault($name, $default)
public function setDefault(string $name, mixed $default): static
{
if ('_locale' === $name && $this->isLocalized()) {
return $this;
}
$this->defaults[$name] = $default;
$this->compiled = null;
return $this;
}
/**
* Returns the requirements.
*
* @return array The requirements
*/
public function getRequirements()
public function getRequirements(): array
{
return $this->requirements;
}
/**
* Sets the requirements.
*
* This method implements a fluent interface.
*
* @param array $requirements The requirements
*
* @return $this
*/
public function setRequirements(array $requirements)
public function setRequirements(array $requirements): static
{
$this->requirements = array();
$this->requirements = [];
return $this->addRequirements($requirements);
}
/**
* Adds requirements.
*
* This method implements a fluent interface.
*
* @param array $requirements The requirements
*
* @return $this
*/
public function addRequirements(array $requirements)
public function addRequirements(array $requirements): static
{
if (isset($requirements['_locale']) && $this->isLocalized()) {
unset($requirements['_locale']);
}
foreach ($requirements as $key => $regex) {
$this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
}
@@ -478,66 +350,40 @@ class Route implements \Serializable
return $this;
}
/**
* Returns the requirement for the given key.
*
* @param string $key The key
*
* @return string|null The regex or null when not given
*/
public function getRequirement($key)
public function getRequirement(string $key): ?string
{
return isset($this->requirements[$key]) ? $this->requirements[$key] : null;
return $this->requirements[$key] ?? null;
}
public function hasRequirement(string $key): bool
{
return \array_key_exists($key, $this->requirements);
}
/**
* 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
*/
public function hasRequirement($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): static
{
if ('_locale' === $key && $this->isLocalized()) {
return $this;
}
$this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
$this->compiled = null;
return $this;
}
/**
* Returns the condition.
*
* @return string The condition
*/
public function getCondition()
public function getCondition(): string
{
return $this->condition;
}
/**
* 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): static
{
$this->condition = (string) $condition;
$this->compiled = null;
@@ -548,14 +394,12 @@ class Route implements \Serializable
/**
* Compiles the route.
*
* @return CompiledRoute A CompiledRoute instance
*
* @throws \LogicException If the Route cannot be compiled because the
* path or host pattern is invalid
*
* @see RouteCompiler which is responsible for the compilation process
*/
public function compile()
public function compile(): CompiledRoute
{
if (null !== $this->compiled) {
return $this->compiled;
@@ -566,18 +410,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) {
@@ -586,4 +450,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 = array();
private array $routes = [];
/**
* @var array
* @var array<string, Alias>
*/
private $resources = array();
private $aliases = [];
/**
* @var array<string, ResourceInterface>
*/
private array $resources = [];
/**
* @var array<string, int>
*/
private array $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,96 +67,120 @@ class RouteCollection implements \IteratorAggregate, \Countable
*
* @see all()
*
* @return \ArrayIterator|Route[] An \ArrayIterator object for iterating over routes
* @return \ArrayIterator<string, Route>
*/
public function getIterator()
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->routes);
return new \ArrayIterator($this->all());
}
/**
* Gets the number of Routes in this collection.
*
* @return int The number of routes
*/
public function count()
public function count(): int
{
return count($this->routes);
return \count($this->routes);
}
/**
* Adds a route.
*
* @param string $name The route name
* @param Route $route A Route instance
*/
public function add($name, Route $route)
public function add(string $name, Route $route, int $priority = 0)
{
unset($this->routes[$name]);
unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]);
$this->routes[$name] = $route;
if ($priority) {
$this->priorities[$name] = $priority;
}
}
/**
* Returns all routes in this collection.
*
* @return Route[] An array of routes
* @return array<string, Route>
*/
public function all()
public function all(): array
{
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
*/
public function get($name)
public function get(string $name): ?Route
{
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;
}
/**
* Removes a route or an array of routes by name from the collection.
*
* @param string|array $name The route name or an array of route names
* @param string|string[] $name The route name or an array of route names
*/
public function remove($name)
public function remove(string|array $name)
{
foreach ((array) $name as $n) {
unset($this->routes[$n]);
unset($this->routes[$n], $this->priorities[$n], $this->aliases[$n]);
}
}
/**
* Adds a route collection at the end of the current set by appending all
* routes of the added collection.
*
* @param RouteCollection $collection A RouteCollection instance
*/
public function addCollection(RouteCollection $collection)
public function addCollection(self $collection)
{
// 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];
}
}
$this->resources = array_merge($this->resources, $collection->getResources());
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) {
$this->addResource($resource);
}
}
/**
* 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(), array $requirements = array())
public function addPrefix(string $prefix, array $defaults = [], array $requirements = [])
{
$prefix = trim(trim($prefix), '/');
@@ -154,13 +196,37 @@ class RouteCollection implements \IteratorAggregate, \Countable
}
/**
* 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
* Adds a prefix to the name of all the routes within in the collection.
*/
public function setHost($pattern, array $defaults = array(), array $requirements = array())
public function addNamePrefix(string $prefix)
{
$prefixedRoutes = [];
$prefixedPriorities = [];
$prefixedAliases = [];
foreach ($this->routes as $name => $route) {
$prefixedRoutes[$prefix.$name] = $route;
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.
*/
public function setHost(?string $pattern, array $defaults = [], array $requirements = [])
{
foreach ($this->routes as $route) {
$route->setHost($pattern);
@@ -173,10 +239,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);
@@ -187,8 +251,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)
{
@@ -203,8 +265,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)
{
@@ -219,8 +279,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)
{
@@ -234,9 +292,9 @@ class RouteCollection implements \IteratorAggregate, \Countable
/**
* Sets the schemes (e.g. 'https') all child routes are restricted to.
*
* @param string|array $schemes The scheme or an array of schemes
* @param string|string[] $schemes The scheme or an array of schemes
*/
public function setSchemes($schemes)
public function setSchemes(string|array $schemes)
{
foreach ($this->routes as $route) {
$route->setSchemes($schemes);
@@ -246,9 +304,9 @@ class RouteCollection implements \IteratorAggregate, \Countable
/**
* Sets the HTTP methods (e.g. 'POST') all child routes are restricted to.
*
* @param string|array $methods The method or an array of methods
* @param string|string[] $methods The method or an array of methods
*/
public function setMethods($methods)
public function setMethods(string|array $methods)
{
foreach ($this->routes as $route) {
$route->setMethods($methods);
@@ -258,20 +316,55 @@ 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()
public function getResources(): array
{
return array_unique($this->resources);
return array_values($this->resources);
}
/**
* Adds a resource for this collection.
*
* @param ResourceInterface $resource A resource instance
* Adds a resource for this collection. If the resource already exists
* it is not added.
*/
public function addResource(ResourceInterface $resource)
{
$this->resources[] = $resource;
$key = (string) $resource;
if (!isset($this->resources[$key])) {
$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

@@ -1,383 +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;
use Symfony\Component\Config\Exception\FileLoaderLoadException;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\ResourceInterface;
/**
* Helps add and import routes into a RouteCollection.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class RouteCollectionBuilder
{
/**
* @var Route[]|RouteCollectionBuilder[]
*/
private $routes = array();
private $loader;
private $defaults = array();
private $prefix;
private $host;
private $condition;
private $requirements = array();
private $options = array();
private $schemes;
private $methods;
private $resources = array();
/**
* @param LoaderInterface $loader
*/
public function __construct(LoaderInterface $loader = null)
{
$this->loader = $loader;
}
/**
* Import an external routing resource and returns the RouteCollectionBuilder.
*
* $routes->import('blog.yml', '/blog');
*
* @param mixed $resource
* @param string|null $prefix
* @param string $type
*
* @return self
*
* @throws FileLoaderLoadException
*/
public function import($resource, $prefix = '/', $type = null)
{
/** @var RouteCollection[] $collection */
$collections = $this->load($resource, $type);
// create a builder from the RouteCollection
$builder = $this->createBuilder();
foreach ($collections as $collection) {
if (null === $collection) {
continue;
}
foreach ($collection->all() as $name => $route) {
$builder->addRoute($route, $name);
}
foreach ($collection->getResources() as $resource) {
$builder->addResource($resource);
}
// mount into this builder
$this->mount($prefix, $builder);
}
return $builder;
}
/**
* 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)
{
$route = new Route($path);
$route->setDefault('_controller', $controller);
$this->addRoute($route, $name);
return $route;
}
/**
* Returns a RouteCollectionBuilder that can be configured and then added with mount().
*
* @return self
*/
public function createBuilder()
{
return new self($this->loader);
}
/**
* Add a RouteCollectionBuilder.
*
* @param string $prefix
* @param RouteCollectionBuilder $builder
*/
public function mount($prefix, RouteCollectionBuilder $builder)
{
$builder->prefix = trim(trim($prefix), '/');
$this->routes[] = $builder;
}
/**
* Adds a Route object to the builder.
*
* @param Route $route
* @param string|null $name
*
* @return $this
*/
public function addRoute(Route $route, $name = null)
{
if (null === $name) {
// used as a flag to know which routes will need a name later
$name = '_unnamed_route_'.spl_object_hash($route);
}
$this->routes[$name] = $route;
return $this;
}
/**
* Sets the host on all embedded routes (unless already set).
*
* @param string $pattern
*
* @return $this
*/
public function setHost($pattern)
{
$this->host = $pattern;
return $this;
}
/**
* Sets a condition on all embedded routes (unless already set).
*
* @param string $condition
*
* @return $this
*/
public function setCondition($condition)
{
$this->condition = $condition;
return $this;
}
/**
* 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
*
* @return $this
*/
public function setDefault($key, $value)
{
$this->defaults[$key] = $value;
return $this;
}
/**
* Sets a requirement that will be added to all embedded routes (unless that
* requirement is already set).
*
* @param string $key
* @param mixed $regex
*
* @return $this
*/
public function setRequirement($key, $regex)
{
$this->requirements[$key] = $regex;
return $this;
}
/**
* Sets an option that will be added to all embedded routes (unless that
* option is already set).
*
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function setOption($key, $value)
{
$this->options[$key] = $value;
return $this;
}
/**
* Sets the schemes on all embedded routes (unless already set).
*
* @param array|string $schemes
*
* @return $this
*/
public function setSchemes($schemes)
{
$this->schemes = $schemes;
return $this;
}
/**
* Sets the methods on all embedded routes (unless already set).
*
* @param array|string $methods
*
* @return $this
*/
public function setMethods($methods)
{
$this->methods = $methods;
return $this;
}
/**
* Adds a resource for this collection.
*
* @param ResourceInterface $resource
*
* @return $this
*/
private function addResource(ResourceInterface $resource)
{
$this->resources[] = $resource;
return $this;
}
/**
* Creates the final RouteCollection and returns it.
*
* @return RouteCollection
*/
public function build()
{
$routeCollection = new RouteCollection();
foreach ($this->routes as $name => $route) {
if ($route instanceof Route) {
$route->setDefaults(array_merge($this->defaults, $route->getDefaults()));
$route->setOptions(array_merge($this->options, $route->getOptions()));
foreach ($this->requirements as $key => $val) {
if (!$route->hasRequirement($key)) {
$route->setRequirement($key, $val);
}
}
if (null !== $this->prefix) {
$route->setPath('/'.$this->prefix.$route->getPath());
}
if (!$route->getHost()) {
$route->setHost($this->host);
}
if (!$route->getCondition()) {
$route->setCondition($this->condition);
}
if (!$route->getSchemes()) {
$route->setSchemes($this->schemes);
}
if (!$route->getMethods()) {
$route->setMethods($this->methods);
}
// auto-generate the route name if it's been marked
if ('_unnamed_route_' === substr($name, 0, 15)) {
$name = $this->generateRouteName($route);
}
$routeCollection->add($name, $route);
} else {
/* @var self $route */
$subCollection = $route->build();
$subCollection->addPrefix($this->prefix);
$routeCollection->addCollection($subCollection);
}
foreach ($this->resources as $resource) {
$routeCollection->addResource($resource);
}
}
return $routeCollection;
}
/**
* Generates a route name based on details of this route.
*
* @return string
*/
private function generateRouteName(Route $route)
{
$methods = implode('_', $route->getMethods()).'_';
$routeName = $methods.$route->getPath();
$routeName = str_replace(array('/', ':', '|', '-'), '_', $routeName);
$routeName = preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
// Collapse consecutive underscores down into a single underscore.
$routeName = preg_replace('/_+/', '_', $routeName);
return $routeName;
}
/**
* Finds a loader able to load an imported resource and loads it.
*
* @param mixed $resource A resource
* @param string|null $type The resource type or null if unknown
*
* @return RouteCollection[]
*
* @throws FileLoaderLoadException If no loader is found
*/
private function load($resource, $type = null)
{
if (null === $this->loader) {
throw new \BadMethodCallException('Cannot import other routing resources: you must pass a LoaderInterface when constructing RouteCollectionBuilder.');
}
if ($this->loader->supports($resource, $type)) {
$collections = $this->loader->load($resource, $type);
return is_array($collections) ? $collections : array($collections);
}
if (null === $resolver = $this->loader->getResolver()) {
throw new FileLoaderLoadException($resource, null, null, null, $type);
}
if (false === $loader = $resolver->resolve($resource, $type)) {
throw new FileLoaderLoadException($resource, null, null, null, $type);
}
$collections = $loader->load($resource, $type);
return is_array($collections) ? $collections : array($collections);
}
}

View File

@@ -19,14 +19,12 @@ namespace Symfony\Component\Routing;
*/
class RouteCompiler implements RouteCompilerInterface
{
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,22 +32,22 @@ class RouteCompiler implements RouteCompilerInterface
*
* @internal
*/
const VARIABLE_MAXIMUM_LENGTH = 32;
public const VARIABLE_MAXIMUM_LENGTH = 32;
/**
* {@inheritdoc}
*
* @throws \InvalidArgumentException If a path variable is named _fragment
* @throws \LogicException If a variable is referenced more than once
* @throws \DomainException If a variable name starts with a digit or if it is too long to be successfully used as
* a PCRE subpattern.
* @throws \InvalidArgumentException if a path variable is named _fragment
* @throws \LogicException if a variable is referenced more than once
* @throws \DomainException if a variable name starts with a digit or if it is too long to be successfully used as
* a PCRE subpattern
*/
public static function compile(Route $route)
public static function compile(Route $route): CompiledRoute
{
$hostVariables = array();
$variables = array();
$hostVariables = [];
$variables = [];
$hostRegex = null;
$hostTokens = array();
$hostTokens = [];
if ('' !== $host = $route->getHost()) {
$result = self::compilePattern($route, $host, true);
@@ -61,6 +59,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,19 +98,18 @@ class RouteCompiler implements RouteCompilerInterface
);
}
private static function compilePattern(Route $route, $pattern, $isHost)
private static function compilePattern(Route $route, string $pattern, bool $isHost): array
{
$tokens = array();
$variables = array();
$matches = array();
$tokens = [];
$variables = [];
$matches = [];
$pos = 0;
$defaultSeparator = $isHost ? '.' : '/';
$useUtf8 = preg_match('//u', $pattern);
$needsUtf8 = $route->getOption('utf8');
if (!$needsUtf8 && $useUtf8 && preg_match('/[\x80-\xFF]/', $pattern)) {
$needsUtf8 = true;
@trigger_error(sprintf('Using UTF-8 route patterns without setting the "utf8" option is deprecated since Symfony 3.2 and will throw a LogicException in 4.0. Turn on the "utf8" route option for pattern "%s".', $pattern), E_USER_DEPRECATED);
throw new \LogicException(sprintf('Cannot use UTF-8 route patterns without setting the "utf8" option for route "%s".', $route->getPath()));
}
if (!$useUtf8 && $needsUtf8) {
throw new \LogicException(sprintf('Cannot mix UTF-8 requirements with non-UTF-8 pattern "%s".', $pattern));
@@ -112,14 +117,15 @@ 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) {
$varName = substr($match[0][0], 1, -1);
$important = $match[1][1] >= 0;
$varName = $match[2][0];
// get all static text preceding the current variable
$precedingText = substr($pattern, $pos, $match[0][1] - $pos);
$pos = $match[0][1] + strlen($match[0][0]);
$pos = $match[0][1] + \strlen($match[0][0]);
if (!strlen($precedingText)) {
if (!\strlen($precedingText)) {
$precedingChar = '';
} elseif ($useUtf8) {
preg_match('/.$/u', $precedingText, $precedingChar);
@@ -127,25 +133,25 @@ 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.
if (preg_match('/^\d/', $varName)) {
throw new \DomainException(sprintf('Variable name "%s" cannot start with a digit in route pattern "%s". Please use a different name.', $varName, $pattern));
}
if (in_array($varName, $variables)) {
if (\in_array($varName, $variables)) {
throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $pattern, $varName));
}
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));
if (\strlen($varName) > self::VARIABLE_MAXIMUM_LENGTH) {
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[] = array('text', substr($precedingText, 0, -strlen($precedingChar)));
} elseif (!$isSeparator && strlen($precedingText) > 0) {
$tokens[] = array('text', $precedingText);
$tokens[] = ['text', substr($precedingText, 0, -\strlen($precedingChar))];
} elseif (!$isSeparator && '' !== $precedingText) {
$tokens[] = ['text', $precedingText];
}
$regexp = $route->getRequirement($varName);
@@ -154,15 +160,15 @@ class RouteCompiler implements RouteCompilerInterface
// Find the next static character after the variable that functions as a separator. By default, this separator and '/'
// are disallowed for the variable. This default requirement makes sure that optional variables can be matched at all
// and that the generating-matching-combination of URLs unambiguous, i.e. the params used for generating the URL are
// the same that will be matched. Example: new Route('/{page}.{_format}', array('_format' => 'html'))
// the same that will be matched. Example: new Route('/{page}.{_format}', ['_format' => 'html'])
// If {page} would also match the separating dot, {_format} would never match as {page} will eagerly consume everything.
// Also even if {_format} was not optional the requirement prevents that {page} matches something that was originally
// part of {_format} when generating the URL, e.g. _format = 'mobile.html'.
$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
@@ -176,28 +182,35 @@ class RouteCompiler implements RouteCompilerInterface
if (!preg_match('//u', $regexp)) {
$useUtf8 = false;
} elseif (!$needsUtf8 && preg_match('/[\x80-\xFF]|(?<!\\\\)\\\\(?:\\\\\\\\)*+(?-i:X|[pP][\{CLMNPSZ]|x\{[A-Fa-f0-9]{3})/', $regexp)) {
$needsUtf8 = true;
@trigger_error(sprintf('Using UTF-8 route requirements without setting the "utf8" option is deprecated since Symfony 3.2 and will throw a LogicException in 4.0. Turn on the "utf8" route option for variable "%s" in pattern "%s".', $varName, $pattern), E_USER_DEPRECATED);
throw new \LogicException(sprintf('Cannot use UTF-8 route requirements without setting the "utf8" option for variable "%s" in pattern "%s".', $varName, $pattern));
}
if (!$useUtf8 && $needsUtf8) {
throw new \LogicException(sprintf('Cannot mix UTF-8 requirement with non-UTF-8 charset for variable "%s" in pattern "%s".', $varName, $pattern));
}
$regexp = self::transformCapturingGroupsToNonCapturings($regexp);
}
$tokens[] = array('variable', $isSeparator ? $precedingChar : '', $regexp, $varName);
if ($important) {
$token = ['variable', $isSeparator ? $precedingChar : '', $regexp, $varName, false, true];
} else {
$token = ['variable', $isSeparator ? $precedingChar : '', $regexp, $varName];
}
$tokens[] = $token;
$variables[] = $varName;
}
if ($pos < strlen($pattern)) {
$tokens[] = array('text', substr($pattern, $pos));
if ($pos < \strlen($pattern)) {
$tokens[] = ['text', substr($pattern, $pos)];
}
// find the first optional token
$firstOptional = PHP_INT_MAX;
$firstOptional = \PHP_INT_MAX;
if (!$isHost) {
for ($i = count($tokens) - 1; $i >= 0; --$i) {
for ($i = \count($tokens) - 1; $i >= 0; --$i) {
$token = $tokens[$i];
if ('variable' === $token[0] && $route->hasDefault($token[3])) {
// variable is optional when it is not important and has a default value
if ('variable' === $token[0] && !($token[5] ?? false) && $route->hasDefault($token[3])) {
$firstOptional = $i;
} else {
break;
@@ -207,38 +220,33 @@ class RouteCompiler implements RouteCompilerInterface
// compute the matching regexp
$regexp = '';
for ($i = 0, $nbToken = count($tokens); $i < $nbToken; ++$i) {
for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) {
$regexp .= self::computeRegexp($tokens, $i, $firstOptional);
}
$regexp = self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s'.($isHost ? 'i' : '');
$regexp = '{^'.$regexp.'$}sD'.($isHost ? 'i' : '');
// enable Utf8 matching if really required
if ($needsUtf8) {
$regexp .= 'u';
for ($i = 0, $nbToken = count($tokens); $i < $nbToken; ++$i) {
for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) {
if ('variable' === $tokens[$i][0]) {
$tokens[$i][] = true;
$tokens[$i][4] = true;
}
}
}
return array(
return [
'staticPrefix' => self::determineStaticPrefix($route, $tokens),
'regex' => $regexp,
'tokens' => array_reverse($tokens),
'variables' => $variables,
);
];
}
/**
* Determines the longest static prefix possible for a route.
*
* @param Route $route
* @param array $tokens
*
* @return string The leading static part of a route's path
*/
private static function determineStaticPrefix(Route $route, array $tokens)
private static function determineStaticPrefix(Route $route, array $tokens): string
{
if ('text' !== $tokens[0][0]) {
return ($route->hasDefault($tokens[0][3]) || '/' === $tokens[0][1]) ? '' : $tokens[0][1];
@@ -254,14 +262,9 @@ class RouteCompiler implements RouteCompilerInterface
}
/**
* Returns the next static character in the Route pattern that will serve as a separator.
*
* @param string $pattern The route pattern
* @param bool $useUtf8 Whether the character is encoded in UTF-8 or not
*
* @return string The next static character that functions as separator (or empty string when none available)
* Returns the next static character in the Route pattern that will serve as a separator (or the empty string when none available).
*/
private static function findNextSeparator($pattern, $useUtf8)
private static function findNextSeparator(string $pattern, bool $useUtf8): string
{
if ('' == $pattern) {
// return empty string if pattern is empty or false (false which can be returned by substr)
@@ -275,7 +278,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] : '';
}
/**
@@ -284,28 +287,26 @@ 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, $index, $firstOptional)
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
// matched the optional subpattern is not passed back.
$regexp = "(?:$regexp";
$nbTokens = count($tokens);
$nbTokens = \count($tokens);
if ($nbTokens - 1 == $index) {
// Close the optional subpatterns
$regexp .= str_repeat(')?', $nbTokens - $firstOptional - (0 === $firstOptional ? 1 : 0));
@@ -316,4 +317,25 @@ class RouteCompiler implements RouteCompilerInterface
}
}
}
private static function transformCapturingGroupsToNonCapturings(string $regexp): string
{
for ($i = 0; $i < \strlen($regexp); ++$i) {
if ('\\' === $regexp[$i]) {
++$i;
continue;
}
if ('(' !== $regexp[$i] || !isset($regexp[$i + 2])) {
continue;
}
if ('*' === $regexp[++$i] || '?' === $regexp[$i]) {
++$i;
continue;
}
$regexp = substr_replace($regexp, '?:', $i, 0);
++$i;
}
return $regexp;
}
}

View File

@@ -21,12 +21,8 @@ interface RouteCompilerInterface
/**
* Compiles the current route instance.
*
* @param Route $route A Route instance
*
* @return CompiledRoute A CompiledRoute instance
*
* @throws \LogicException If the Route cannot be compiled because the
* path or host pattern is invalid
*/
public static function compile(Route $route);
public static function compile(Route $route): CompiledRoute;
}

View File

@@ -11,19 +11,23 @@
namespace Symfony\Component\Routing;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\ConfigCacheInterface;
use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\Config\ConfigCacheFactory;
use Psr\Log\LoggerInterface;
use Symfony\Component\Config\ConfigCacheFactory;
use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\Config\ConfigCacheInterface;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\CompiledUrlGenerator;
use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper;
use Symfony\Component\Routing\Generator\Dumper\GeneratorDumperInterface;
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\UrlMatcherInterface;
use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
/**
* The Router class is an example of the integration of all pieces of the
@@ -66,7 +70,7 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* @var array
*/
protected $options = array();
protected $options = [];
/**
* @var LoggerInterface|null
@@ -74,31 +78,27 @@ class Router implements RouterInterface, RequestMatcherInterface
protected $logger;
/**
* @var ConfigCacheFactoryInterface|null
* @var string|null
*/
protected $defaultLocale;
private $configCacheFactory;
/**
* @var ExpressionFunctionProviderInterface[]
*/
private $expressionLanguageProviders = array();
private array $expressionLanguageProviders = [];
/**
* Constructor.
*
* @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
*/
public function __construct(LoaderInterface $loader, $resource, array $options = array(), RequestContext $context = null, LoggerInterface $logger = null)
private static ?array $cache = [];
public function __construct(LoaderInterface $loader, mixed $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;
}
/**
@@ -109,42 +109,32 @@ class Router implements RouterInterface, RequestMatcherInterface
* * cache_dir: The cache directory (or null to disable caching)
* * debug: Whether to enable debugging or not (false by default)
* * generator_class: The name of a UrlGeneratorInterface implementation
* * generator_base_class: The base class for the dumped generator class
* * generator_cache_class: The class name for the dumped generator class
* * generator_dumper_class: The name of a GeneratorDumperInterface implementation
* * matcher_class: The name of a UrlMatcherInterface implementation
* * matcher_base_class: The base class for the dumped matcher class
* * matcher_dumper_class: The class name for the dumped matcher class
* * matcher_cache_class: The name of a MatcherDumperInterface implementation
* * matcher_dumper_class: The name of a MatcherDumperInterface implementation
* * resource_type: Type hint for the main resource (optional)
* * 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)
{
$this->options = array(
$this->options = [
'cache_dir' => null,
'debug' => false,
'generator_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
'generator_base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
'generator_dumper_class' => 'Symfony\\Component\\Routing\\Generator\\Dumper\\PhpGeneratorDumper',
'generator_cache_class' => 'ProjectUrlGenerator',
'matcher_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
'matcher_base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
'matcher_dumper_class' => 'Symfony\\Component\\Routing\\Matcher\\Dumper\\PhpMatcherDumper',
'matcher_cache_class' => 'ProjectUrlMatcher',
'generator_class' => CompiledUrlGenerator::class,
'generator_dumper_class' => CompiledUrlGeneratorDumper::class,
'matcher_class' => CompiledUrlMatcher::class,
'matcher_dumper_class' => CompiledUrlMatcherDumper::class,
'resource_type' => null,
'strict_requirements' => true,
);
];
// check option names and live merge, if errors are encountered Exception will be thrown
$invalid = array();
$invalid = [];
foreach ($options as $key => $value) {
if (array_key_exists($key, $this->options)) {
if (\array_key_exists($key, $this->options)) {
$this->options[$key] = $value;
} else {
$invalid[] = $key;
@@ -159,14 +149,11 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* Sets an option.
*
* @param string $key The key
* @param mixed $value The value
*
* @throws \InvalidArgumentException
*/
public function setOption($key, $value)
public function setOption(string $key, mixed $value)
{
if (!array_key_exists($key, $this->options)) {
if (!\array_key_exists($key, $this->options)) {
throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key));
}
@@ -176,15 +163,11 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* Gets an option value.
*
* @param string $key The key
*
* @return mixed The value
*
* @throws \InvalidArgumentException
*/
public function getOption($key)
public function getOption(string $key): mixed
{
if (!array_key_exists($key, $this->options)) {
if (!\array_key_exists($key, $this->options)) {
throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key));
}
@@ -221,15 +204,13 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* {@inheritdoc}
*/
public function getContext()
public function getContext(): RequestContext
{
return $this->context;
}
/**
* Sets the ConfigCache factory to use.
*
* @param ConfigCacheFactoryInterface $configCacheFactory The factory to use
*/
public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
{
@@ -239,7 +220,7 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* {@inheritdoc}
*/
public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
{
return $this->getGenerator()->generate($name, $parameters, $referenceType);
}
@@ -247,7 +228,7 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* {@inheritdoc}
*/
public function match($pathinfo)
public function match(string $pathinfo): array
{
return $this->getMatcher()->match($pathinfo);
}
@@ -255,7 +236,7 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* {@inheritdoc}
*/
public function matchRequest(Request $request)
public function matchRequest(Request $request): array
{
$matcher = $this->getMatcher();
if (!$matcher instanceof RequestMatcherInterface) {
@@ -267,18 +248,21 @@ class Router implements RouterInterface, RequestMatcherInterface
}
/**
* Gets the UrlMatcher instance associated with this Router.
*
* @return UrlMatcherInterface A UrlMatcherInterface instance
* Gets the UrlMatcher or RequestMatcher instance associated with this Router.
*/
public function getMatcher()
public function getMatcher(): UrlMatcherInterface|RequestMatcherInterface
{
if (null !== $this->matcher) {
return $this->matcher;
}
if (null === $this->options['cache_dir'] || null === $this->options['matcher_cache_class']) {
$this->matcher = new $this->options['matcher_class']($this->getRouteCollection(), $this->context);
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();
}
$this->matcher = new $this->options['matcher_class']($routes, $this->context);
if (method_exists($this->matcher, 'addExpressionLanguageProvider')) {
foreach ($this->expressionLanguageProviders as $provider) {
$this->matcher->addExpressionLanguageProvider($provider);
@@ -288,7 +272,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')) {
@@ -297,50 +281,40 @@ class Router implements RouterInterface, RequestMatcherInterface
}
}
$options = array(
'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());
}
);
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
*/
public function getGenerator()
public function getGenerator(): UrlGeneratorInterface
{
if (null !== $this->generator) {
return $this->generator;
}
if (null === $this->options['cache_dir'] || null === $this->options['generator_cache_class']) {
$this->generator = new $this->options['generator_class']($this->getRouteCollection(), $this->context, $this->logger);
if (null === $this->options['cache_dir']) {
$routes = $this->getRouteCollection();
$compiled = is_a($this->options['generator_class'], CompiledUrlGenerator::class, true);
if ($compiled) {
$generatorDumper = new CompiledUrlGeneratorDumper($routes);
$routes = array_merge($generatorDumper->getCompiledRoutes(), $generatorDumper->getCompiledAliases());
}
$this->generator = new $this->options['generator_class']($routes, $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 = array(
'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());
}
);
require_once $cache->getPath();
$this->generator = new $this->options['generator_cache_class']($this->context, $this->logger);
$this->generator = new $this->options['generator_class'](self::getCompiledRoutes($cache->getPath()), $this->context, $this->logger, $this->defaultLocale);
}
if ($this->generator instanceof ConfigurableRequirementsInterface) {
@@ -355,18 +329,12 @@ class Router implements RouterInterface, RequestMatcherInterface
$this->expressionLanguageProviders[] = $provider;
}
/**
* @return GeneratorDumperInterface
*/
protected function getGeneratorDumperInstance()
protected function getGeneratorDumperInstance(): GeneratorDumperInterface
{
return new $this->options['generator_dumper_class']($this->getRouteCollection());
}
/**
* @return MatcherDumperInterface
*/
protected function getMatcherDumperInstance()
protected function getMatcherDumperInstance(): MatcherDumperInterface
{
return new $this->options['matcher_dumper_class']($this->getRouteCollection());
}
@@ -374,15 +342,22 @@ class Router implements RouterInterface, RequestMatcherInterface
/**
* Provides the ConfigCache factory implementation, falling back to a
* default implementation if necessary.
*
* @return ConfigCacheFactoryInterface $configCacheFactory
*/
private function getConfigCacheFactory()
private function getConfigCacheFactory(): ConfigCacheFactoryInterface
{
if (null === $this->configCacheFactory) {
$this->configCacheFactory = new ConfigCacheFactory($this->options['debug']);
return $this->configCacheFactory ??= new ConfigCacheFactory($this->options['debug']);
}
private static function getCompiledRoutes(string $path): array
{
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;
}
return $this->configCacheFactory;
if (null === self::$cache) {
return require $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,50 +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(array('foo' => 'bar'));
}
/**
* @dataProvider getValidParameters
*/
public function testRouteParameters($parameter, $value, $getter)
{
$route = new Route(array($parameter => $value));
$this->assertEquals($route->$getter(), $value);
}
public function getValidParameters()
{
return array(
array('value', '/Blog', 'getPath'),
array('requirements', array('locale' => 'en'), 'getRequirements'),
array('options', array('compiler_class' => 'RouteCompiler'), 'getOptions'),
array('name', 'blog_index', 'getName'),
array('defaults', array('_controller' => 'MyBlogBundle:Blog:index'), 'getDefaults'),
array('schemes', array('https'), 'getSchemes'),
array('methods', array('GET', 'POST'), 'getMethods'),
array('host', '{locale}.example.com', 'getHost'),
array('condition', 'context.getMethod() == "GET"', 'getCondition'),
);
}
}

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', array('tokens'), array(), array(), array(), array(), array('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(array('tokens'), $compiled->getTokens(), '__construct() takes an array of tokens as its fourth argument');
$this->assertEquals(array('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(
array(array('addLoader', array(new Reference('loader1'))), array('addLoader', array(new Reference('loader2')))),
$container->getDefinition('routing.resolver')->getMethodCalls()
);
}
}

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,16 +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 FooClass
{
}

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,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('', '', array(), array());
}
}

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\Loader\XmlFileLoader;
use Symfony\Component\Config\Util\XmlUtils;
/**
* XmlFileLoader with schema validation turned off.
*/
class CustomXmlFileLoader extends XmlFileLoader
{
protected function loadFile($file)
{
return XmlUtils::loadFile($file, function () { return true; });
}
}

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\UrlMatcher;
use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface
{
public function redirect($path, $route, $scheme = null)
{
return array(
'_controller' => 'Some controller reference...',
'path' => $path,
'scheme' => $scheme,
);
}
}

View File

@@ -1,3 +0,0 @@
blog_show:
path: /blog/{slug}
defaults: { _controller: "MyBundle:Blog:show" }

View File

@@ -1,2 +0,0 @@
route1:
path: /route/1

View File

@@ -1,2 +0,0 @@
route2:
path: /route/2

View File

@@ -1,2 +0,0 @@
route3:
path: /route/3

View File

@@ -1,3 +0,0 @@
_directory:
resource: "../directory"
type: directory

View File

@@ -1,163 +0,0 @@
# skip "real" requests
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule .* - [QSA,L]
# foo
RewriteCond %{REQUEST_URI} ^/foo/(baz|symfony)$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:foo,E=_ROUTING_param_bar:%1,E=_ROUTING_default_def:test]
# foobar
RewriteCond %{REQUEST_URI} ^/foo(?:/([^/]++))?$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:foobar,E=_ROUTING_param_bar:%1,E=_ROUTING_default_bar:toto]
# bar
RewriteCond %{REQUEST_URI} ^/bar/([^/]++)$
RewriteCond %{REQUEST_METHOD} !^(GET|HEAD)$ [NC]
RewriteRule .* - [S=1,E=_ROUTING_allow_GET:1,E=_ROUTING_allow_HEAD:1]
RewriteCond %{REQUEST_URI} ^/bar/([^/]++)$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:bar,E=_ROUTING_param_foo:%1]
# baragain
RewriteCond %{REQUEST_URI} ^/baragain/([^/]++)$
RewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$ [NC]
RewriteRule .* - [S=1,E=_ROUTING_allow_GET:1,E=_ROUTING_allow_POST:1,E=_ROUTING_allow_HEAD:1]
RewriteCond %{REQUEST_URI} ^/baragain/([^/]++)$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baragain,E=_ROUTING_param_foo:%1]
# baz
RewriteCond %{REQUEST_URI} ^/test/baz$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz]
# baz2
RewriteCond %{REQUEST_URI} ^/test/baz\.html$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz2]
# baz3
RewriteCond %{REQUEST_URI} ^/test/baz3$
RewriteRule .* $0/ [QSA,L,R=301]
RewriteCond %{REQUEST_URI} ^/test/baz3/$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz3]
# baz4
RewriteCond %{REQUEST_URI} ^/test/([^/]++)$
RewriteRule .* $0/ [QSA,L,R=301]
RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz4,E=_ROUTING_param_foo:%1]
# baz5
RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
RewriteCond %{REQUEST_METHOD} !^(GET|HEAD)$ [NC]
RewriteRule .* - [S=2,E=_ROUTING_allow_GET:1,E=_ROUTING_allow_HEAD:1]
RewriteCond %{REQUEST_URI} ^/test/([^/]++)$
RewriteRule .* $0/ [QSA,L,R=301]
RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz5,E=_ROUTING_param_foo:%1]
# baz5unsafe
RewriteCond %{REQUEST_URI} ^/testunsafe/([^/]++)/$
RewriteCond %{REQUEST_METHOD} !^(POST)$ [NC]
RewriteRule .* - [S=1,E=_ROUTING_allow_POST:1]
RewriteCond %{REQUEST_URI} ^/testunsafe/([^/]++)/$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz5unsafe,E=_ROUTING_param_foo:%1]
# baz6
RewriteCond %{REQUEST_URI} ^/test/baz$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz6,E=_ROUTING_default_foo:bar\ baz]
# baz7
RewriteCond %{REQUEST_URI} ^/te\ st/baz$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz7]
# baz8
RewriteCond %{REQUEST_URI} ^/te\\\ st/baz$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz8]
# baz9
RewriteCond %{REQUEST_URI} ^/test/(te\\\ st)$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz9,E=_ROUTING_param_baz:%1]
RewriteCond %{HTTP:Host} ^a\.example\.com$
RewriteRule .? - [E=__ROUTING_host_1:1]
# route1
RewriteCond %{ENV:__ROUTING_host_1} =1
RewriteCond %{REQUEST_URI} ^/route1$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route1]
# route2
RewriteCond %{ENV:__ROUTING_host_1} =1
RewriteCond %{REQUEST_URI} ^/c2/route2$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route2]
RewriteCond %{HTTP:Host} ^b\.example\.com$
RewriteRule .? - [E=__ROUTING_host_2:1]
# route3
RewriteCond %{ENV:__ROUTING_host_2} =1
RewriteCond %{REQUEST_URI} ^/c2/route3$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route3]
RewriteCond %{HTTP:Host} ^a\.example\.com$
RewriteRule .? - [E=__ROUTING_host_3:1]
# route4
RewriteCond %{ENV:__ROUTING_host_3} =1
RewriteCond %{REQUEST_URI} ^/route4$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route4]
RewriteCond %{HTTP:Host} ^c\.example\.com$
RewriteRule .? - [E=__ROUTING_host_4:1]
# route5
RewriteCond %{ENV:__ROUTING_host_4} =1
RewriteCond %{REQUEST_URI} ^/route5$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route5]
# route6
RewriteCond %{REQUEST_URI} ^/route6$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route6]
RewriteCond %{HTTP:Host} ^([^\.]++)\.example\.com$
RewriteRule .? - [E=__ROUTING_host_5:1,E=__ROUTING_host_5_var1:%1]
# route11
RewriteCond %{ENV:__ROUTING_host_5} =1
RewriteCond %{REQUEST_URI} ^/route11$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route11,E=_ROUTING_param_var1:%{ENV:__ROUTING_host_5_var1}]
# route12
RewriteCond %{ENV:__ROUTING_host_5} =1
RewriteCond %{REQUEST_URI} ^/route12$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route12,E=_ROUTING_param_var1:%{ENV:__ROUTING_host_5_var1},E=_ROUTING_default_var1:val]
# route13
RewriteCond %{ENV:__ROUTING_host_5} =1
RewriteCond %{REQUEST_URI} ^/route13/([^/]++)$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route13,E=_ROUTING_param_var1:%{ENV:__ROUTING_host_5_var1},E=_ROUTING_param_name:%1]
# route14
RewriteCond %{ENV:__ROUTING_host_5} =1
RewriteCond %{REQUEST_URI} ^/route14/([^/]++)$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route14,E=_ROUTING_param_var1:%{ENV:__ROUTING_host_5_var1},E=_ROUTING_param_name:%1,E=_ROUTING_default_var1:val]
RewriteCond %{HTTP:Host} ^c\.example\.com$
RewriteRule .? - [E=__ROUTING_host_6:1]
# route15
RewriteCond %{ENV:__ROUTING_host_6} =1
RewriteCond %{REQUEST_URI} ^/route15/([^/]++)$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route15,E=_ROUTING_param_name:%1]
# route16
RewriteCond %{REQUEST_URI} ^/route16/([^/]++)$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route16,E=_ROUTING_param_name:%1,E=_ROUTING_default_var1:val]
# route17
RewriteCond %{REQUEST_URI} ^/route17$
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route17]
# 405 Method Not Allowed
RewriteCond %{ENV:_ROUTING__allow_GET} =1 [OR]
RewriteCond %{ENV:_ROUTING__allow_HEAD} =1 [OR]
RewriteCond %{ENV:_ROUTING__allow_POST} =1
RewriteRule .* app.php [QSA,L]

View File

@@ -1,317 +0,0 @@
<?php
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;
/**
* ProjectUrlMatcher.
*
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
/**
* Constructor.
*/
public function __construct(RequestContext $context)
{
$this->context = $context;
}
public function match($pathinfo)
{
$allow = array();
$pathinfo = rawurldecode($pathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$request = $this->request;
$requestMethod = $canonicalMethod = $context->getMethod();
$scheme = $context->getScheme();
if ('HEAD' === $requestMethod) {
$canonicalMethod = 'GET';
}
if (0 === strpos($pathinfo, '/foo')) {
// foo
if (preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo')), array ( 'def' => 'test',));
}
// foofoo
if ('/foofoo' === $pathinfo) {
return array ( 'def' => 'test', '_route' => 'foofoo',);
}
}
elseif (0 === strpos($pathinfo, '/bar')) {
// bar
if (preg_match('#^/bar/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_bar;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar')), array ());
}
not_bar:
// barhead
if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_barhead;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'barhead')), array ());
}
not_barhead:
}
elseif (0 === strpos($pathinfo, '/test')) {
if (0 === strpos($pathinfo, '/test/baz')) {
// baz
if ('/test/baz' === $pathinfo) {
return array('_route' => 'baz');
}
// baz2
if ('/test/baz.html' === $pathinfo) {
return array('_route' => 'baz2');
}
// baz3
if ('/test/baz3/' === $pathinfo) {
return array('_route' => 'baz3');
}
}
// baz4
if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz4')), array ());
}
// baz5
if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) {
if ('POST' !== $canonicalMethod) {
$allow[] = 'POST';
goto not_baz5;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz5')), array ());
}
not_baz5:
// baz.baz6
if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) {
if ('PUT' !== $canonicalMethod) {
$allow[] = 'PUT';
goto not_bazbaz6;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz.baz6')), array ());
}
not_bazbaz6:
}
// quoter
if (preg_match('#^/(?P<quoter>[\']+)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'quoter')), array ());
}
// space
if ('/spa ce' === $pathinfo) {
return array('_route' => 'space');
}
if (0 === strpos($pathinfo, '/a')) {
if (0 === strpos($pathinfo, '/a/b\'b')) {
// foo1
if (preg_match('#^/a/b\'b/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo1')), array ());
}
// bar1
if (preg_match('#^/a/b\'b/(?P<bar>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar1')), array ());
}
}
// overridden
if (preg_match('#^/a/(?P<var>.*)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'overridden')), array ());
}
if (0 === strpos($pathinfo, '/a/b\'b')) {
// foo2
if (preg_match('#^/a/b\'b/(?P<foo1>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array ());
}
// bar2
if (preg_match('#^/a/b\'b/(?P<bar1>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar2')), array ());
}
}
}
elseif (0 === strpos($pathinfo, '/multi')) {
// helloWorld
if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?P<who>[^/]++))?$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'helloWorld')), array ( 'who' => 'World!',));
}
// hey
if ('/multi/hey/' === $pathinfo) {
return array('_route' => 'hey');
}
// overridden2
if ('/multi/new' === $pathinfo) {
return array('_route' => 'overridden2');
}
}
// foo3
if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo3')), array ());
}
// bar3
if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P<bar>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar3')), array ());
}
if (0 === strpos($pathinfo, '/aba')) {
// ababa
if ('/ababa' === $pathinfo) {
return array('_route' => 'ababa');
}
// foo4
if (preg_match('#^/aba/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo4')), array ());
}
}
$host = $context->getHost();
if (preg_match('#^a\\.example\\.com$#si', $host, $hostMatches)) {
// route1
if ('/route1' === $pathinfo) {
return array('_route' => 'route1');
}
// route2
if ('/c2/route2' === $pathinfo) {
return array('_route' => 'route2');
}
}
if (preg_match('#^b\\.example\\.com$#si', $host, $hostMatches)) {
// route3
if ('/c2/route3' === $pathinfo) {
return array('_route' => 'route3');
}
}
if (preg_match('#^a\\.example\\.com$#si', $host, $hostMatches)) {
// route4
if ('/route4' === $pathinfo) {
return array('_route' => 'route4');
}
}
if (preg_match('#^c\\.example\\.com$#si', $host, $hostMatches)) {
// route5
if ('/route5' === $pathinfo) {
return array('_route' => 'route5');
}
}
// route6
if ('/route6' === $pathinfo) {
return array('_route' => 'route6');
}
if (preg_match('#^(?P<var1>[^\\.]++)\\.example\\.com$#si', $host, $hostMatches)) {
if (0 === strpos($pathinfo, '/route1')) {
// route11
if ('/route11' === $pathinfo) {
return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route11')), array ());
}
// route12
if ('/route12' === $pathinfo) {
return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route12')), array ( 'var1' => 'val',));
}
// route13
if (0 === strpos($pathinfo, '/route13') && preg_match('#^/route13/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route13')), array ());
}
// route14
if (0 === strpos($pathinfo, '/route14') && preg_match('#^/route14/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route14')), array ( 'var1' => 'val',));
}
}
}
if (preg_match('#^c\\.example\\.com$#si', $host, $hostMatches)) {
// route15
if (0 === strpos($pathinfo, '/route15') && preg_match('#^/route15/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'route15')), array ());
}
}
// route16
if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'route16')), array ( 'var1' => 'val',));
}
// route17
if ('/route17' === $pathinfo) {
return array('_route' => 'route17');
}
// a
if ('/a/a...' === $pathinfo) {
return array('_route' => 'a');
}
if (0 === strpos($pathinfo, '/a/b')) {
// b
if (preg_match('#^/a/b/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'b')), array ());
}
// c
if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'c')), array ());
}
}
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
}
}

View File

@@ -1,7 +0,0 @@
# skip "real" requests
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule .* - [QSA,L]
# foo
RewriteCond %{REQUEST_URI} ^/foo$
RewriteRule .* ap\ p_d\ ev.php [QSA,L,E=_ROUTING_route:foo]

View File

@@ -1,349 +0,0 @@
<?php
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;
/**
* ProjectUrlMatcher.
*
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher
{
/**
* Constructor.
*/
public function __construct(RequestContext $context)
{
$this->context = $context;
}
public function match($pathinfo)
{
$allow = array();
$pathinfo = rawurldecode($pathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$request = $this->request;
$requestMethod = $canonicalMethod = $context->getMethod();
$scheme = $context->getScheme();
if ('HEAD' === $requestMethod) {
$canonicalMethod = 'GET';
}
if (0 === strpos($pathinfo, '/foo')) {
// foo
if (preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo')), array ( 'def' => 'test',));
}
// foofoo
if ('/foofoo' === $pathinfo) {
return array ( 'def' => 'test', '_route' => 'foofoo',);
}
}
elseif (0 === strpos($pathinfo, '/bar')) {
// bar
if (preg_match('#^/bar/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_bar;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar')), array ());
}
not_bar:
// barhead
if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_barhead;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'barhead')), array ());
}
not_barhead:
}
elseif (0 === strpos($pathinfo, '/test')) {
if (0 === strpos($pathinfo, '/test/baz')) {
// baz
if ('/test/baz' === $pathinfo) {
return array('_route' => 'baz');
}
// baz2
if ('/test/baz.html' === $pathinfo) {
return array('_route' => 'baz2');
}
// baz3
if ('/test/baz3' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'baz3');
}
return array('_route' => 'baz3');
}
}
// baz4
if (preg_match('#^/test/(?P<foo>[^/]++)/?$#s', $pathinfo, $matches)) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'baz4');
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz4')), array ());
}
// baz5
if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) {
if ('POST' !== $canonicalMethod) {
$allow[] = 'POST';
goto not_baz5;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz5')), array ());
}
not_baz5:
// baz.baz6
if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) {
if ('PUT' !== $canonicalMethod) {
$allow[] = 'PUT';
goto not_bazbaz6;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz.baz6')), array ());
}
not_bazbaz6:
}
// quoter
if (preg_match('#^/(?P<quoter>[\']+)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'quoter')), array ());
}
// space
if ('/spa ce' === $pathinfo) {
return array('_route' => 'space');
}
if (0 === strpos($pathinfo, '/a')) {
if (0 === strpos($pathinfo, '/a/b\'b')) {
// foo1
if (preg_match('#^/a/b\'b/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo1')), array ());
}
// bar1
if (preg_match('#^/a/b\'b/(?P<bar>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar1')), array ());
}
}
// overridden
if (preg_match('#^/a/(?P<var>.*)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'overridden')), array ());
}
if (0 === strpos($pathinfo, '/a/b\'b')) {
// foo2
if (preg_match('#^/a/b\'b/(?P<foo1>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array ());
}
// bar2
if (preg_match('#^/a/b\'b/(?P<bar1>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar2')), array ());
}
}
}
elseif (0 === strpos($pathinfo, '/multi')) {
// helloWorld
if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?P<who>[^/]++))?$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'helloWorld')), array ( 'who' => 'World!',));
}
// hey
if ('/multi/hey' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'hey');
}
return array('_route' => 'hey');
}
// overridden2
if ('/multi/new' === $pathinfo) {
return array('_route' => 'overridden2');
}
}
// foo3
if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo3')), array ());
}
// bar3
if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P<bar>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar3')), array ());
}
if (0 === strpos($pathinfo, '/aba')) {
// ababa
if ('/ababa' === $pathinfo) {
return array('_route' => 'ababa');
}
// foo4
if (preg_match('#^/aba/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo4')), array ());
}
}
$host = $context->getHost();
if (preg_match('#^a\\.example\\.com$#si', $host, $hostMatches)) {
// route1
if ('/route1' === $pathinfo) {
return array('_route' => 'route1');
}
// route2
if ('/c2/route2' === $pathinfo) {
return array('_route' => 'route2');
}
}
if (preg_match('#^b\\.example\\.com$#si', $host, $hostMatches)) {
// route3
if ('/c2/route3' === $pathinfo) {
return array('_route' => 'route3');
}
}
if (preg_match('#^a\\.example\\.com$#si', $host, $hostMatches)) {
// route4
if ('/route4' === $pathinfo) {
return array('_route' => 'route4');
}
}
if (preg_match('#^c\\.example\\.com$#si', $host, $hostMatches)) {
// route5
if ('/route5' === $pathinfo) {
return array('_route' => 'route5');
}
}
// route6
if ('/route6' === $pathinfo) {
return array('_route' => 'route6');
}
if (preg_match('#^(?P<var1>[^\\.]++)\\.example\\.com$#si', $host, $hostMatches)) {
if (0 === strpos($pathinfo, '/route1')) {
// route11
if ('/route11' === $pathinfo) {
return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route11')), array ());
}
// route12
if ('/route12' === $pathinfo) {
return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route12')), array ( 'var1' => 'val',));
}
// route13
if (0 === strpos($pathinfo, '/route13') && preg_match('#^/route13/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route13')), array ());
}
// route14
if (0 === strpos($pathinfo, '/route14') && preg_match('#^/route14/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route14')), array ( 'var1' => 'val',));
}
}
}
if (preg_match('#^c\\.example\\.com$#si', $host, $hostMatches)) {
// route15
if (0 === strpos($pathinfo, '/route15') && preg_match('#^/route15/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'route15')), array ());
}
}
// route16
if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'route16')), array ( 'var1' => 'val',));
}
// route17
if ('/route17' === $pathinfo) {
return array('_route' => 'route17');
}
// a
if ('/a/a...' === $pathinfo) {
return array('_route' => 'a');
}
if (0 === strpos($pathinfo, '/a/b')) {
// b
if (preg_match('#^/a/b/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'b')), array ());
}
// c
if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'c')), array ());
}
}
// secure
if ('/secure' === $pathinfo) {
$requiredSchemes = array ( 'https' => 0,);
if (!isset($requiredSchemes[$scheme])) {
return $this->redirect($pathinfo, 'secure', key($requiredSchemes));
}
return array('_route' => 'secure');
}
// nonsecure
if ('/nonsecure' === $pathinfo) {
$requiredSchemes = array ( 'http' => 0,);
if (!isset($requiredSchemes[$scheme])) {
return $this->redirect($pathinfo, 'nonsecure', key($requiredSchemes));
}
return array('_route' => 'nonsecure');
}
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
}
}

View File

@@ -1,58 +0,0 @@
<?php
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;
/**
* ProjectUrlMatcher.
*
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
/**
* Constructor.
*/
public function __construct(RequestContext $context)
{
$this->context = $context;
}
public function match($pathinfo)
{
$allow = array();
$pathinfo = rawurldecode($pathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$request = $this->request;
$requestMethod = $canonicalMethod = $context->getMethod();
$scheme = $context->getScheme();
if ('HEAD' === $requestMethod) {
$canonicalMethod = 'GET';
}
if (0 === strpos($pathinfo, '/rootprefix')) {
// static
if ('/rootprefix/test' === $pathinfo) {
return array('_route' => 'static');
}
// dynamic
if (preg_match('#^/rootprefix/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'dynamic')), array ());
}
}
// with-condition
if ('/with-condition' === $pathinfo && ($context->getMethod() == "GET")) {
return array('_route' => 'with-condition');
}
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
}
}

View File

@@ -1,98 +0,0 @@
<?php
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;
/**
* ProjectUrlMatcher.
*
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
/**
* Constructor.
*/
public function __construct(RequestContext $context)
{
$this->context = $context;
}
public function match($pathinfo)
{
$allow = array();
$pathinfo = rawurldecode($pathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$request = $this->request;
$requestMethod = $canonicalMethod = $context->getMethod();
$scheme = $context->getScheme();
if ('HEAD' === $requestMethod) {
$canonicalMethod = 'GET';
}
// just_head
if ('/just_head' === $pathinfo) {
if ('HEAD' !== $requestMethod) {
$allow[] = 'HEAD';
goto not_just_head;
}
return array('_route' => 'just_head');
}
not_just_head:
// head_and_get
if ('/head_and_get' === $pathinfo) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_head_and_get;
}
return array('_route' => 'head_and_get');
}
not_head_and_get:
// post_and_head
if ('/post_and_get' === $pathinfo) {
if (!in_array($requestMethod, array('POST', 'HEAD'))) {
$allow = array_merge($allow, array('POST', 'HEAD'));
goto not_post_and_head;
}
return array('_route' => 'post_and_head');
}
not_post_and_head:
if (0 === strpos($pathinfo, '/put_and_post')) {
// put_and_post
if ('/put_and_post' === $pathinfo) {
if (!in_array($requestMethod, array('PUT', 'POST'))) {
$allow = array_merge($allow, array('PUT', 'POST'));
goto not_put_and_post;
}
return array('_route' => 'put_and_post');
}
not_put_and_post:
// put_and_get_and_head
if ('/put_and_post' === $pathinfo) {
if (!in_array($canonicalMethod, array('PUT', 'GET'))) {
$allow = array_merge($allow, array('PUT', 'GET'));
goto not_put_and_get_and_head;
}
return array('_route' => 'put_and_get_and_head');
}
not_put_and_get_and_head:
}
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
}
}

View File

@@ -1,158 +0,0 @@
<?php
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;
/**
* ProjectUrlMatcher.
*
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher
{
/**
* Constructor.
*/
public function __construct(RequestContext $context)
{
$this->context = $context;
}
public function match($pathinfo)
{
$allow = array();
$pathinfo = rawurldecode($pathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$request = $this->request;
$requestMethod = $canonicalMethod = $context->getMethod();
$scheme = $context->getScheme();
if ('HEAD' === $requestMethod) {
$canonicalMethod = 'GET';
}
if (0 === strpos($pathinfo, '/a')) {
// a_first
if ('/a/11' === $pathinfo) {
return array('_route' => 'a_first');
}
// a_second
if ('/a/22' === $pathinfo) {
return array('_route' => 'a_second');
}
// a_third
if ('/a/333' === $pathinfo) {
return array('_route' => 'a_third');
}
}
// a_wildcard
if (preg_match('#^/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'a_wildcard')), array ());
}
if (0 === strpos($pathinfo, '/a')) {
// a_fourth
if ('/a/44' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'a_fourth');
}
return array('_route' => 'a_fourth');
}
// a_fifth
if ('/a/55' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'a_fifth');
}
return array('_route' => 'a_fifth');
}
// a_sixth
if ('/a/66' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'a_sixth');
}
return array('_route' => 'a_sixth');
}
}
// nested_wildcard
if (0 === strpos($pathinfo, '/nested') && preg_match('#^/nested/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'nested_wildcard')), array ());
}
if (0 === strpos($pathinfo, '/nested/group')) {
// nested_a
if ('/nested/group/a' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'nested_a');
}
return array('_route' => 'nested_a');
}
// nested_b
if ('/nested/group/b' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'nested_b');
}
return array('_route' => 'nested_b');
}
// nested_c
if ('/nested/group/c' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'nested_c');
}
return array('_route' => 'nested_c');
}
}
elseif (0 === strpos($pathinfo, '/slashed/group')) {
// slashed_a
if ('/slashed/group' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'slashed_a');
}
return array('_route' => 'slashed_a');
}
// slashed_b
if ('/slashed/group/b' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'slashed_b');
}
return array('_route' => 'slashed_b');
}
// slashed_c
if ('/slashed/group/c' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'slashed_c');
}
return array('_route' => 'slashed_c');
}
}
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
}
}

View File

@@ -1,204 +0,0 @@
<?php
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;
/**
* ProjectUrlMatcher.
*
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
/**
* Constructor.
*/
public function __construct(RequestContext $context)
{
$this->context = $context;
}
public function match($pathinfo)
{
$allow = array();
$pathinfo = rawurldecode($pathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$request = $this->request;
$requestMethod = $canonicalMethod = $context->getMethod();
$scheme = $context->getScheme();
if ('HEAD' === $requestMethod) {
$canonicalMethod = 'GET';
}
if (0 === strpos($pathinfo, '/trailing/simple')) {
// simple_trailing_slash_no_methods
if ('/trailing/simple/no-methods/' === $pathinfo) {
return array('_route' => 'simple_trailing_slash_no_methods');
}
// simple_trailing_slash_GET_method
if ('/trailing/simple/get-method/' === $pathinfo) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_simple_trailing_slash_GET_method;
}
return array('_route' => 'simple_trailing_slash_GET_method');
}
not_simple_trailing_slash_GET_method:
// simple_trailing_slash_HEAD_method
if ('/trailing/simple/head-method/' === $pathinfo) {
if ('HEAD' !== $requestMethod) {
$allow[] = 'HEAD';
goto not_simple_trailing_slash_HEAD_method;
}
return array('_route' => 'simple_trailing_slash_HEAD_method');
}
not_simple_trailing_slash_HEAD_method:
// simple_trailing_slash_POST_method
if ('/trailing/simple/post-method/' === $pathinfo) {
if ('POST' !== $canonicalMethod) {
$allow[] = 'POST';
goto not_simple_trailing_slash_POST_method;
}
return array('_route' => 'simple_trailing_slash_POST_method');
}
not_simple_trailing_slash_POST_method:
}
elseif (0 === strpos($pathinfo, '/trailing/regex')) {
// regex_trailing_slash_no_methods
if (0 === strpos($pathinfo, '/trailing/regex/no-methods') && preg_match('#^/trailing/regex/no\\-methods/(?P<param>[^/]++)/$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_no_methods')), array ());
}
// regex_trailing_slash_GET_method
if (0 === strpos($pathinfo, '/trailing/regex/get-method') && preg_match('#^/trailing/regex/get\\-method/(?P<param>[^/]++)/$#s', $pathinfo, $matches)) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_regex_trailing_slash_GET_method;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_GET_method')), array ());
}
not_regex_trailing_slash_GET_method:
// regex_trailing_slash_HEAD_method
if (0 === strpos($pathinfo, '/trailing/regex/head-method') && preg_match('#^/trailing/regex/head\\-method/(?P<param>[^/]++)/$#s', $pathinfo, $matches)) {
if ('HEAD' !== $requestMethod) {
$allow[] = 'HEAD';
goto not_regex_trailing_slash_HEAD_method;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_HEAD_method')), array ());
}
not_regex_trailing_slash_HEAD_method:
// regex_trailing_slash_POST_method
if (0 === strpos($pathinfo, '/trailing/regex/post-method') && preg_match('#^/trailing/regex/post\\-method/(?P<param>[^/]++)/$#s', $pathinfo, $matches)) {
if ('POST' !== $canonicalMethod) {
$allow[] = 'POST';
goto not_regex_trailing_slash_POST_method;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_POST_method')), array ());
}
not_regex_trailing_slash_POST_method:
}
elseif (0 === strpos($pathinfo, '/not-trailing/simple')) {
// simple_not_trailing_slash_no_methods
if ('/not-trailing/simple/no-methods' === $pathinfo) {
return array('_route' => 'simple_not_trailing_slash_no_methods');
}
// simple_not_trailing_slash_GET_method
if ('/not-trailing/simple/get-method' === $pathinfo) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_simple_not_trailing_slash_GET_method;
}
return array('_route' => 'simple_not_trailing_slash_GET_method');
}
not_simple_not_trailing_slash_GET_method:
// simple_not_trailing_slash_HEAD_method
if ('/not-trailing/simple/head-method' === $pathinfo) {
if ('HEAD' !== $requestMethod) {
$allow[] = 'HEAD';
goto not_simple_not_trailing_slash_HEAD_method;
}
return array('_route' => 'simple_not_trailing_slash_HEAD_method');
}
not_simple_not_trailing_slash_HEAD_method:
// simple_not_trailing_slash_POST_method
if ('/not-trailing/simple/post-method' === $pathinfo) {
if ('POST' !== $canonicalMethod) {
$allow[] = 'POST';
goto not_simple_not_trailing_slash_POST_method;
}
return array('_route' => 'simple_not_trailing_slash_POST_method');
}
not_simple_not_trailing_slash_POST_method:
}
elseif (0 === strpos($pathinfo, '/not-trailing/regex')) {
// regex_not_trailing_slash_no_methods
if (0 === strpos($pathinfo, '/not-trailing/regex/no-methods') && preg_match('#^/not\\-trailing/regex/no\\-methods/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_no_methods')), array ());
}
// regex_not_trailing_slash_GET_method
if (0 === strpos($pathinfo, '/not-trailing/regex/get-method') && preg_match('#^/not\\-trailing/regex/get\\-method/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_regex_not_trailing_slash_GET_method;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_GET_method')), array ());
}
not_regex_not_trailing_slash_GET_method:
// regex_not_trailing_slash_HEAD_method
if (0 === strpos($pathinfo, '/not-trailing/regex/head-method') && preg_match('#^/not\\-trailing/regex/head\\-method/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
if ('HEAD' !== $requestMethod) {
$allow[] = 'HEAD';
goto not_regex_not_trailing_slash_HEAD_method;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_HEAD_method')), array ());
}
not_regex_not_trailing_slash_HEAD_method:
// regex_not_trailing_slash_POST_method
if (0 === strpos($pathinfo, '/not-trailing/regex/post-method') && preg_match('#^/not\\-trailing/regex/post\\-method/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
if ('POST' !== $canonicalMethod) {
$allow[] = 'POST';
goto not_regex_not_trailing_slash_POST_method;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_POST_method')), array ());
}
not_regex_not_trailing_slash_POST_method:
}
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
}
}

View File

@@ -1,228 +0,0 @@
<?php
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;
/**
* ProjectUrlMatcher.
*
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher
{
/**
* Constructor.
*/
public function __construct(RequestContext $context)
{
$this->context = $context;
}
public function match($pathinfo)
{
$allow = array();
$pathinfo = rawurldecode($pathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$request = $this->request;
$requestMethod = $canonicalMethod = $context->getMethod();
$scheme = $context->getScheme();
if ('HEAD' === $requestMethod) {
$canonicalMethod = 'GET';
}
if (0 === strpos($pathinfo, '/trailing/simple')) {
// simple_trailing_slash_no_methods
if ('/trailing/simple/no-methods' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'simple_trailing_slash_no_methods');
}
return array('_route' => 'simple_trailing_slash_no_methods');
}
// simple_trailing_slash_GET_method
if ('/trailing/simple/get-method' === $trimmedPathinfo) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_simple_trailing_slash_GET_method;
}
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'simple_trailing_slash_GET_method');
}
return array('_route' => 'simple_trailing_slash_GET_method');
}
not_simple_trailing_slash_GET_method:
// simple_trailing_slash_HEAD_method
if ('/trailing/simple/head-method' === $trimmedPathinfo) {
if ('HEAD' !== $requestMethod) {
$allow[] = 'HEAD';
goto not_simple_trailing_slash_HEAD_method;
}
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'simple_trailing_slash_HEAD_method');
}
return array('_route' => 'simple_trailing_slash_HEAD_method');
}
not_simple_trailing_slash_HEAD_method:
// simple_trailing_slash_POST_method
if ('/trailing/simple/post-method/' === $pathinfo) {
if ('POST' !== $canonicalMethod) {
$allow[] = 'POST';
goto not_simple_trailing_slash_POST_method;
}
return array('_route' => 'simple_trailing_slash_POST_method');
}
not_simple_trailing_slash_POST_method:
}
elseif (0 === strpos($pathinfo, '/trailing/regex')) {
// regex_trailing_slash_no_methods
if (0 === strpos($pathinfo, '/trailing/regex/no-methods') && preg_match('#^/trailing/regex/no\\-methods/(?P<param>[^/]++)/?$#s', $pathinfo, $matches)) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'regex_trailing_slash_no_methods');
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_no_methods')), array ());
}
// regex_trailing_slash_GET_method
if (0 === strpos($pathinfo, '/trailing/regex/get-method') && preg_match('#^/trailing/regex/get\\-method/(?P<param>[^/]++)/?$#s', $pathinfo, $matches)) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_regex_trailing_slash_GET_method;
}
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'regex_trailing_slash_GET_method');
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_GET_method')), array ());
}
not_regex_trailing_slash_GET_method:
// regex_trailing_slash_HEAD_method
if (0 === strpos($pathinfo, '/trailing/regex/head-method') && preg_match('#^/trailing/regex/head\\-method/(?P<param>[^/]++)/?$#s', $pathinfo, $matches)) {
if ('HEAD' !== $requestMethod) {
$allow[] = 'HEAD';
goto not_regex_trailing_slash_HEAD_method;
}
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'regex_trailing_slash_HEAD_method');
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_HEAD_method')), array ());
}
not_regex_trailing_slash_HEAD_method:
// regex_trailing_slash_POST_method
if (0 === strpos($pathinfo, '/trailing/regex/post-method') && preg_match('#^/trailing/regex/post\\-method/(?P<param>[^/]++)/$#s', $pathinfo, $matches)) {
if ('POST' !== $canonicalMethod) {
$allow[] = 'POST';
goto not_regex_trailing_slash_POST_method;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_POST_method')), array ());
}
not_regex_trailing_slash_POST_method:
}
elseif (0 === strpos($pathinfo, '/not-trailing/simple')) {
// simple_not_trailing_slash_no_methods
if ('/not-trailing/simple/no-methods' === $pathinfo) {
return array('_route' => 'simple_not_trailing_slash_no_methods');
}
// simple_not_trailing_slash_GET_method
if ('/not-trailing/simple/get-method' === $pathinfo) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_simple_not_trailing_slash_GET_method;
}
return array('_route' => 'simple_not_trailing_slash_GET_method');
}
not_simple_not_trailing_slash_GET_method:
// simple_not_trailing_slash_HEAD_method
if ('/not-trailing/simple/head-method' === $pathinfo) {
if ('HEAD' !== $requestMethod) {
$allow[] = 'HEAD';
goto not_simple_not_trailing_slash_HEAD_method;
}
return array('_route' => 'simple_not_trailing_slash_HEAD_method');
}
not_simple_not_trailing_slash_HEAD_method:
// simple_not_trailing_slash_POST_method
if ('/not-trailing/simple/post-method' === $pathinfo) {
if ('POST' !== $canonicalMethod) {
$allow[] = 'POST';
goto not_simple_not_trailing_slash_POST_method;
}
return array('_route' => 'simple_not_trailing_slash_POST_method');
}
not_simple_not_trailing_slash_POST_method:
}
elseif (0 === strpos($pathinfo, '/not-trailing/regex')) {
// regex_not_trailing_slash_no_methods
if (0 === strpos($pathinfo, '/not-trailing/regex/no-methods') && preg_match('#^/not\\-trailing/regex/no\\-methods/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_no_methods')), array ());
}
// regex_not_trailing_slash_GET_method
if (0 === strpos($pathinfo, '/not-trailing/regex/get-method') && preg_match('#^/not\\-trailing/regex/get\\-method/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
if ('GET' !== $canonicalMethod) {
$allow[] = 'GET';
goto not_regex_not_trailing_slash_GET_method;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_GET_method')), array ());
}
not_regex_not_trailing_slash_GET_method:
// regex_not_trailing_slash_HEAD_method
if (0 === strpos($pathinfo, '/not-trailing/regex/head-method') && preg_match('#^/not\\-trailing/regex/head\\-method/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
if ('HEAD' !== $requestMethod) {
$allow[] = 'HEAD';
goto not_regex_not_trailing_slash_HEAD_method;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_HEAD_method')), array ());
}
not_regex_not_trailing_slash_HEAD_method:
// regex_not_trailing_slash_POST_method
if (0 === strpos($pathinfo, '/not-trailing/regex/post-method') && preg_match('#^/not\\-trailing/regex/post\\-method/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
if ('POST' !== $canonicalMethod) {
$allow[] = 'POST';
goto not_regex_not_trailing_slash_POST_method;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_POST_method')), array ());
}
not_regex_not_trailing_slash_POST_method:
}
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
}
}

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