File manager - Edit - /home/autoph/public_html/projects/Rating-AutoHub/public/css/routing.tar
Back
RouteCollection.php 0000644 00000024061 15025056712 0010375 0 ustar 00 <?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\Resource\ResourceInterface; use Symfony\Component\Routing\Exception\InvalidArgumentException; use Symfony\Component\Routing\Exception\RouteCircularReferenceException; /** * A RouteCollection represents a set of Route instances. * * When adding a route at the end of the collection, an existing route * with the same name is removed first. So there can only be one route * with a given name. * * @author Fabien Potencier <fabien@symfony.com> * @author Tobias Schultze <http://tobion.de> * * @implements \IteratorAggregate<string, Route> */ class RouteCollection implements \IteratorAggregate, \Countable { /** * @var array<string, Route> */ private array $routes = []; /** * @var array<string, Alias> */ 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; } } /** * Gets the current RouteCollection as an Iterator that includes all routes. * * It implements \IteratorAggregate. * * @see all() * * @return \ArrayIterator<string, Route> */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->all()); } /** * Gets the number of Routes in this collection. */ public function count(): int { return \count($this->routes); } public function add(string $name, Route $route, int $priority = 0) { 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 array<string, Route> */ 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. */ public function get(string $name): ?Route { $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|string[] $name The route name or an array of route names */ public function remove(string|array $name) { foreach ((array) $name as $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. */ 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], $this->priorities[$name], $this->aliases[$name]); $this->routes[$name] = $route; if (isset($collection->priorities[$name])) { $this->priorities[$name] = $collection->priorities[$name]; } } foreach ($collection->getAliases() as $name => $alias) { unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]); $this->aliases[$name] = $alias; } foreach ($collection->getResources() as $resource) { $this->addResource($resource); } } /** * Adds a prefix to the path of all child routes. */ public function addPrefix(string $prefix, array $defaults = [], array $requirements = []) { $prefix = trim(trim($prefix), '/'); if ('' === $prefix) { return; } foreach ($this->routes as $route) { $route->setPath('/'.$prefix.$route->getPath()); $route->addDefaults($defaults); $route->addRequirements($requirements); } } /** * Adds a prefix to the name of all the routes within in the collection. */ 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); $route->addDefaults($defaults); $route->addRequirements($requirements); } } /** * Sets a condition on all routes. * * Existing conditions will be overridden. */ public function setCondition(?string $condition) { foreach ($this->routes as $route) { $route->setCondition($condition); } } /** * Adds defaults to all routes. * * An existing default value under the same name in a route will be overridden. */ public function addDefaults(array $defaults) { if ($defaults) { foreach ($this->routes as $route) { $route->addDefaults($defaults); } } } /** * Adds requirements to all routes. * * An existing requirement under the same name in a route will be overridden. */ public function addRequirements(array $requirements) { if ($requirements) { foreach ($this->routes as $route) { $route->addRequirements($requirements); } } } /** * Adds options to all routes. * * An existing option value under the same name in a route will be overridden. */ public function addOptions(array $options) { if ($options) { foreach ($this->routes as $route) { $route->addOptions($options); } } } /** * Sets the schemes (e.g. 'https') all child routes are restricted to. * * @param string|string[] $schemes The scheme or an array of schemes */ public function setSchemes(string|array $schemes) { foreach ($this->routes as $route) { $route->setSchemes($schemes); } } /** * Sets the HTTP methods (e.g. 'POST') all child routes are restricted to. * * @param string|string[] $methods The method or an array of methods */ public function setMethods(string|array $methods) { foreach ($this->routes as $route) { $route->setMethods($methods); } } /** * Returns an array of resources loaded to build this collection. * * @return ResourceInterface[] */ public function getResources(): array { return array_values($this->resources); } /** * Adds a resource for this collection. If the resource already exists * it is not added. */ public function addResource(ResourceInterface $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; } } RequestContext.php 0000644 00000015115 15025056712 0010260 0 ustar 00 <?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\HttpFoundation\Request; /** * Holds information about the current request. * * This class implements a fluent interface. * * @author Fabien Potencier <fabien@symfony.com> * @author Tobias Schultze <http://tobion.de> */ class RequestContext { 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 = []; 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); $this->setHost($host); $this->setScheme($scheme); $this->setHttpPort($httpPort); $this->setHttpsPort($httpsPort); $this->setPathInfo($path); $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. * * @return $this */ 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() || 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; } /** * Gets the base URL. */ public function getBaseUrl(): string { return $this->baseUrl; } /** * Sets the base URL. * * @return $this */ public function setBaseUrl(string $baseUrl): static { $this->baseUrl = rtrim($baseUrl, '/'); return $this; } /** * Gets the path info. */ public function getPathInfo(): string { return $this->pathInfo; } /** * Sets the path info. * * @return $this */ public function setPathInfo(string $pathInfo): static { $this->pathInfo = $pathInfo; return $this; } /** * Gets the HTTP method. * * The method is always an uppercased string. */ public function getMethod(): string { return $this->method; } /** * Sets the HTTP method. * * @return $this */ public function setMethod(string $method): static { $this->method = strtoupper($method); return $this; } /** * Gets the HTTP host. * * The host is always lowercased because it must be treated case-insensitive. */ public function getHost(): string { return $this->host; } /** * Sets the HTTP host. * * @return $this */ public function setHost(string $host): static { $this->host = strtolower($host); return $this; } /** * Gets the HTTP scheme. */ public function getScheme(): string { return $this->scheme; } /** * Sets the HTTP scheme. * * @return $this */ public function setScheme(string $scheme): static { $this->scheme = strtolower($scheme); return $this; } /** * Gets the HTTP port. */ public function getHttpPort(): int { return $this->httpPort; } /** * Sets the HTTP port. * * @return $this */ public function setHttpPort(int $httpPort): static { $this->httpPort = $httpPort; return $this; } /** * Gets the HTTPS port. */ public function getHttpsPort(): int { return $this->httpsPort; } /** * Sets the HTTPS port. * * @return $this */ public function setHttpsPort(int $httpsPort): static { $this->httpsPort = $httpsPort; return $this; } /** * Gets the query string without the "?". */ public function getQueryString(): string { return $this->queryString; } /** * Sets the query string. * * @return $this */ public function setQueryString(?string $queryString): static { // string cast to be fault-tolerant, accepting null $this->queryString = (string) $queryString; return $this; } /** * Returns the parameters. */ public function getParameters(): array { return $this->parameters; } /** * Sets the parameters. * * @param array $parameters The parameters * * @return $this */ public function setParameters(array $parameters): static { $this->parameters = $parameters; return $this; } /** * Gets a parameter value. */ public function getParameter(string $name): mixed { return $this->parameters[$name] ?? null; } /** * Checks if a parameter value is set for the given parameter. */ public function hasParameter(string $name): bool { return \array_key_exists($name, $this->parameters); } /** * Sets a parameter value. * * @return $this */ public function setParameter(string $name, mixed $parameter): static { $this->parameters[$name] = $parameter; return $this; } public function isSecure(): bool { return 'https' === $this->scheme; } } DependencyInjection/RoutingResolverPass.php 0000644 00000002267 15025056712 0015210 0 ustar 00 <?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\DependencyInjection; 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. * * @author Fabien Potencier <fabien@symfony.com> */ class RoutingResolverPass implements CompilerPassInterface { use PriorityTaggedServiceTrait; public function process(ContainerBuilder $container) { if (false === $container->hasDefinition('routing.resolver')) { return; } $definition = $container->getDefinition('routing.resolver'); foreach ($this->findAndSortTaggedServices('routing.loader', $container) as $id) { $definition->addMethodCall('addLoader', [new Reference($id)]); } } } RouteCompilerInterface.php 0000644 00000001335 15025056712 0011674 0 ustar 00 <?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; /** * RouteCompilerInterface is the interface that all RouteCompiler classes must implement. * * @author Fabien Potencier <fabien@symfony.com> */ interface RouteCompilerInterface { /** * Compiles the current route instance. * * @throws \LogicException If the Route cannot be compiled because the * path or host pattern is invalid */ public static function compile(Route $route): CompiledRoute; } composer.json 0000644 00000002776 15025056712 0007305 0 ustar 00 { "name": "symfony/routing", "type": "library", "description": "Maps an HTTP request to a set of configuration variables", "keywords": ["routing", "router", "URL", "URI"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2" }, "require-dev": { "symfony/config": "^5.4|^6.0", "symfony/http-foundation": "^5.4|^6.0", "symfony/yaml": "^5.4|^6.0", "symfony/expression-language": "^5.4|^6.0", "symfony/dependency-injection": "^5.4|^6.0", "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3" }, "conflict": { "doctrine/annotations": "<1.12", "symfony/config": "<5.4", "symfony/dependency-injection": "<5.4", "symfony/yaml": "<5.4" }, "suggest": { "symfony/http-foundation": "For using a Symfony Request object", "symfony/config": "For using the all-in-one router or any loader", "symfony/yaml": "For using the YAML loader", "symfony/expression-language": "For using expression matching" }, "autoload": { "psr-4": { "Symfony\\Component\\Routing\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } CHANGELOG.md 0000644 00000027766 15025056712 0006402 0 ustar 00 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 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 ----- * allowed specifying a directory to recursively load all routing configuration files it contains * Added ObjectRouteLoader and ServiceRouteLoader that allow routes to be loaded by calling a method on an object/service. * [DEPRECATION] Deprecated the hardcoded value for the `$referenceType` argument of the `UrlGeneratorInterface::generate` method. Use the constants defined in the `UrlGeneratorInterface` instead. Before: ```php $router->generate('blog_show', ['slug' => 'my-blog-post'], true); ``` After: ```php use Symfony\Component\Routing\Generator\UrlGeneratorInterface; $router->generate('blog_show', ['slug' => 'my-blog-post'], UrlGeneratorInterface::ABSOLUTE_URL); ``` 2.5.0 ----- * [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 behavior of PHP implementation. 2.3.0 ----- * added RequestContext::getQueryString() 2.2.0 ----- * [DEPRECATION] Several route settings have been renamed (the old ones will be removed in 3.0): * The `pattern` setting for a route has been deprecated in favor of `path` * The `_scheme` and `_method` requirements have been moved to the `schemes` and `methods` settings Before: ```yaml article_edit: pattern: /article/{id} requirements: { '_method': 'POST|PUT', '_scheme': 'https', 'id': '\d+' } ``` ```xml <route id="article_edit" pattern="/article/{id}"> <requirement key="_method">POST|PUT</requirement> <requirement key="_scheme">https</requirement> <requirement key="id">\d+</requirement> </route> ``` ```php $route = new Route(); $route->setPattern('/article/{id}'); $route->setRequirement('_method', 'POST|PUT'); $route->setRequirement('_scheme', 'https'); ``` After: ```yaml article_edit: path: /article/{id} methods: [POST, PUT] schemes: https requirements: { 'id': '\d+' } ``` ```xml <route id="article_edit" pattern="/article/{id}" methods="POST PUT" schemes="https"> <requirement key="id">\d+</requirement> </route> ``` ```php $route = new Route(); $route->setPath('/article/{id}'); $route->setMethods(['POST', 'PUT']); $route->setSchemes('https'); ``` * [BC BREAK] RouteCollection does not behave like a tree structure anymore but as a flat array of Routes. So when using PHP to build the RouteCollection, you must make sure to add routes to the sub-collection before adding it to the parent collection (this is not relevant when using YAML or XML for Route definitions). Before: ```php $rootCollection = new RouteCollection(); $subCollection = new RouteCollection(); $rootCollection->addCollection($subCollection); $subCollection->add('foo', new Route('/foo')); ``` After: ```php $rootCollection = new RouteCollection(); $subCollection = new RouteCollection(); $subCollection->add('foo', new Route('/foo')); $rootCollection->addCollection($subCollection); ``` Also one must call `addCollection` from the bottom to the top hierarchy. So the correct sequence is the following (and not the reverse): ```php $childCollection->addCollection($grandchildCollection); $rootCollection->addCollection($childCollection); ``` * [DEPRECATION] The methods `RouteCollection::getParent()` and `RouteCollection::getRoot()` have been deprecated and will be removed in Symfony 2.3. * [BC BREAK] Misusing the `RouteCollection::addPrefix` method to add defaults, requirements or options without adding a prefix is not supported anymore. So if you called `addPrefix` with an empty prefix or `/` only (both have no relevance), like `addPrefix('', $defaultsArray, $requirementsArray, $optionsArray)` you need to use the new dedicated methods `addDefaults($defaultsArray)`, `addRequirements($requirementsArray)` or `addOptions($optionsArray)` instead. * [DEPRECATION] The `$options` parameter to `RouteCollection::addPrefix()` has been deprecated because adding options has nothing to do with adding a path prefix. If you want to add options to all child routes of a RouteCollection, you can use `addOptions()`. * [DEPRECATION] The method `RouteCollection::getPrefix()` has been deprecated because it suggested that all routes in the collection would have this prefix, which is not necessarily true. On top of that, since there is no tree structure anymore, this method is also useless. Don't worry about performance, prefix optimization for matching is still done in the dumper, which was also improved in 2.2.0 to find even more grouping possibilities. * [DEPRECATION] `RouteCollection::addCollection(RouteCollection $collection)` should now only be 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', [...], [...])` After: ```php $collection->addPrefix('/prefix', [...], [...]); $parentCollection->addCollection($collection); ``` * added support for the method default argument values when defining a @Route * Adjacent placeholders without separator work now, e.g. `/{x}{y}{z}.{_format}`. * Characters that function as separator between placeholders are now whitelisted to fix routes with normal text around a variable, e.g. `/prefix{var}suffix`. * [BC BREAK] The default requirement of a variable has been changed slightly. Previously it disallowed the previous and the next char around a variable. Now it disallows the slash (`/`) and the next char. Using the previous char added no value and was problematic because the route `/index.{_format}` would be matched by `/index.ht/ml`. * The default requirement now uses possessive quantifiers when possible which improves matching performance by up to 20% because it prevents backtracking when it's not needed. * The ConfigurableRequirementsInterface can now also be used to disable the requirements check on URL generation completely by calling `setStrictRequirements(null)`. It improves performance in production environment as you should know that params always pass the requirements (otherwise it would break your link anyway). * There is no restriction on the route name anymore. So non-alphanumeric characters are now also allowed. * [BC BREAK] `RouteCompilerInterface::compile(Route $route)` was made static (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 = [], $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. 2.1.0 ----- * added RequestMatcherInterface * added RequestContext::fromRequest() * the UrlMatcher does not throw a \LogicException anymore when the required scheme is not the current one * added TraceableUrlMatcher * added the possibility to define options, default values and requirements for placeholders in prefix, including imported routes * added RouterInterface::getRouteCollection * [BC BREAK] the UrlMatcher urldecodes the route parameters only once, they were decoded twice before. Note that the `urldecode()` calls have been changed for a single `rawurldecode()` in order to support `+` for input paths. * added RouteCollection::getRoot method to retrieve the root of a RouteCollection tree * [BC BREAK] made RouteCollection::setParent private which could not have been used anyway without creating inconsistencies * [BC BREAK] RouteCollection::remove also removes a route from parent collections (not only from its children) * added ConfigurableRequirementsInterface that allows to disable exceptions (and generate empty URLs instead) when generating a route with an invalid parameter value Exception/RouteCircularReferenceException.php 0000644 00000001056 15025056712 0015501 0 ustar 00 <?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))); } } Exception/ResourceNotFoundException.php 0000644 00000001044 15025056712 0014340 0 ustar 00 <?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; /** * The resource was not found. * * This exception should trigger an HTTP 404 response in your application code. * * @author Kris Wallsmith <kris@symfony.com> */ class ResourceNotFoundException extends \RuntimeException implements ExceptionInterface { } Exception/MissingMandatoryParametersException.php 0000644 00000001062 15025056712 0016410 0 ustar 00 <?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; /** * Exception thrown when a route cannot be generated because of missing * mandatory parameters. * * @author Alexandre Salomé <alexandre.salome@gmail.com> */ class MissingMandatoryParametersException extends \InvalidArgumentException implements ExceptionInterface { } Exception/MethodNotAllowedException.php 0000644 00000002142 15025056712 0014305 0 ustar 00 <?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; /** * The resource was found but the request method is not allowed. * * This exception should trigger an HTTP 405 response in your application code. * * @author Kris Wallsmith <kris@symfony.com> */ class MethodNotAllowedException extends \RuntimeException implements ExceptionInterface { protected $allowedMethods = []; /** * @param string[] $allowedMethods */ public function __construct(array $allowedMethods, string $message = '', int $code = 0, \Throwable $previous = null) { $this->allowedMethods = array_map('strtoupper', $allowedMethods); parent::__construct($message, $code, $previous); } /** * Gets the allowed HTTP methods. * * @return string[] */ public function getAllowedMethods(): array { return $this->allowedMethods; } } Exception/RouteNotFoundException.php 0000644 00000000765 15025056712 0013660 0 ustar 00 <?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; /** * Exception thrown when a route does not exist. * * @author Alexandre Salomé <alexandre.salome@gmail.com> */ class RouteNotFoundException extends \InvalidArgumentException implements ExceptionInterface { } Exception/InvalidArgumentException.php 0000644 00000000601 15025056712 0014163 0 ustar 00 <?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 InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } Exception/ExceptionInterface.php 0000644 00000000656 15025056712 0013004 0 ustar 00 <?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; /** * ExceptionInterface. * * @author Alexandre Salomé <alexandre.salome@gmail.com> */ interface ExceptionInterface extends \Throwable { } Exception/NoConfigurationException.php 0000644 00000000721 15025056712 0014201 0 ustar 00 <?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; /** * Exception thrown when no routes are configured. * * @author Yonel Ceruto <yonelceruto@gmail.com> */ class NoConfigurationException extends ResourceNotFoundException { } Exception/InvalidParameterException.php 0000644 00000000772 15025056712 0014332 0 ustar 00 <?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; /** * Exception thrown when a parameter is not valid. * * @author Alexandre Salomé <alexandre.salome@gmail.com> */ class InvalidParameterException extends \InvalidArgumentException implements ExceptionInterface { } Exception/RuntimeException.php 0000644 00000000561 15025056712 0012522 0 ustar 00 <?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 RuntimeException extends \RuntimeException implements ExceptionInterface { } RequestContextAwareInterface.php 0000644 00000001015 15025056712 0013053 0 ustar 00 <?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; interface RequestContextAwareInterface { /** * Sets the request context. */ public function setContext(RequestContext $context); /** * Gets the request context. */ public function getContext(): RequestContext; } Annotation/Route.php 0000644 00000010165 15025056712 0010473 0 ustar 00 <?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\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 ?string $path = null; private array $localizedPaths = []; private array $methods; private array $schemes; /** * @param string[] $requirements * @param string[]|string $methods * @param string[]|string $schemes */ 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; } if (null !== $format) { $this->defaults['_format'] = $format; } if (null !== $utf8) { $this->options['utf8'] = $utf8; } if (null !== $stateless) { $this->defaults['_stateless'] = $stateless; } } public function setPath(string $path) { $this->path = $path; } public function getPath() { return $this->path; } public function setLocalizedPaths(array $localizedPaths) { $this->localizedPaths = $localizedPaths; } public function getLocalizedPaths(): array { return $this->localizedPaths; } public function setHost(string $pattern) { $this->host = $pattern; } public function getHost() { return $this->host; } public function setName(string $name) { $this->name = $name; } public function getName() { return $this->name; } public function setRequirements(array $requirements) { $this->requirements = $requirements; } public function getRequirements() { return $this->requirements; } public function setOptions(array $options) { $this->options = $options; } public function getOptions() { return $this->options; } public function setDefaults(array $defaults) { $this->defaults = $defaults; } public function getDefaults() { return $this->defaults; } public function setSchemes(array|string $schemes) { $this->schemes = (array) $schemes; } public function getSchemes() { return $this->schemes; } public function setMethods(array|string $methods) { $this->methods = (array) $methods; } public function getMethods() { return $this->methods; } public function setCondition(?string $condition) { $this->condition = $condition; } public function getCondition() { 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; } } Alias.php 0000644 00000005002 15025056712 0006306 0 ustar 00 <?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']), ]; } } Route.php 0000644 00000027625 15025056712 0006372 0 ustar 00 <?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; /** * A Route describes a route and its parameters. * * @author Fabien Potencier <fabien@symfony.com> * @author Tobias Schultze <http://tobion.de> */ class Route implements \Serializable { 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. * * Available options: * * * 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|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(string $path, array $defaults = [], array $requirements = [], array $options = [], ?string $host = '', string|array $schemes = [], string|array $methods = [], ?string $condition = '') { $this->setPath($path); $this->addDefaults($defaults); $this->addRequirements($requirements); $this->setOptions($options); $this->setHost($host); $this->setSchemes($schemes); $this->setMethods($methods); $this->setCondition($condition); } public function __serialize(): array { return [ 'path' => $this->path, 'host' => $this->host, 'defaults' => $this->defaults, 'requirements' => $this->requirements, 'options' => $this->options, 'schemes' => $this->schemes, 'methods' => $this->methods, 'condition' => $this->condition, 'compiled' => $this->compiled, ]; } /** * @internal */ final public function serialize(): string { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } public function __unserialize(array $data): void { $this->path = $data['path']; $this->host = $data['host']; $this->defaults = $data['defaults']; $this->requirements = $data['requirements']; $this->options = $data['options']; $this->schemes = $data['schemes']; $this->methods = $data['methods']; if (isset($data['condition'])) { $this->condition = $data['condition']; } if (isset($data['compiled'])) { $this->compiled = $data['compiled']; } } /** * @internal */ final public function unserialize(string $serialized) { $this->__unserialize(unserialize($serialized)); } public function getPath(): string { return $this->path; } /** * @return $this */ 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), '/'); $this->compiled = null; return $this; } public function getHost(): string { return $this->host; } /** * @return $this */ public function setHost(?string $pattern): static { $this->host = $this->extractInlineDefaultsAndRequirements((string) $pattern); $this->compiled = null; return $this; } /** * Returns the lowercased schemes this route is restricted to. * So an empty array means that any scheme is allowed. * * @return string[] */ public function getSchemes(): array { return $this->schemes; } /** * Sets the schemes (e.g. 'https') this route is restricted to. * So an empty array means that any scheme is allowed. * * @param string|string[] $schemes The scheme or an array of schemes * * @return $this */ public function setSchemes(string|array $schemes): static { $this->schemes = array_map('strtolower', (array) $schemes); $this->compiled = null; return $this; } /** * Checks if a scheme requirement has been set. */ public function hasScheme(string $scheme): bool { 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 string[] */ public function getMethods(): array { return $this->methods; } /** * Sets the HTTP methods (e.g. 'POST') this route is restricted to. * So an empty array means that any method is allowed. * * @param string|string[] $methods The method or an array of methods * * @return $this */ public function setMethods(string|array $methods): static { $this->methods = array_map('strtoupper', (array) $methods); $this->compiled = null; return $this; } public function getOptions(): array { return $this->options; } /** * @return $this */ public function setOptions(array $options): static { $this->options = [ 'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler', ]; return $this->addOptions($options); } /** * @return $this */ public function addOptions(array $options): static { foreach ($options as $name => $option) { $this->options[$name] = $option; } $this->compiled = null; return $this; } /** * Sets an option value. * * @return $this */ public function setOption(string $name, mixed $value): static { $this->options[$name] = $value; $this->compiled = null; return $this; } /** * Returns the option value or null when not found. */ public function getOption(string $name): mixed { return $this->options[$name] ?? null; } public function hasOption(string $name): bool { return \array_key_exists($name, $this->options); } public function getDefaults(): array { return $this->defaults; } /** * @return $this */ public function setDefaults(array $defaults): static { $this->defaults = []; return $this->addDefaults($defaults); } /** * @return $this */ public function addDefaults(array $defaults): static { if (isset($defaults['_locale']) && $this->isLocalized()) { unset($defaults['_locale']); } foreach ($defaults as $name => $default) { $this->defaults[$name] = $default; } $this->compiled = null; return $this; } public function getDefault(string $name): mixed { return $this->defaults[$name] ?? null; } public function hasDefault(string $name): bool { return \array_key_exists($name, $this->defaults); } /** * @return $this */ public function setDefault(string $name, mixed $default): static { if ('_locale' === $name && $this->isLocalized()) { return $this; } $this->defaults[$name] = $default; $this->compiled = null; return $this; } public function getRequirements(): array { return $this->requirements; } /** * @return $this */ public function setRequirements(array $requirements): static { $this->requirements = []; return $this->addRequirements($requirements); } /** * @return $this */ 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); } $this->compiled = null; return $this; } public function getRequirement(string $key): ?string { return $this->requirements[$key] ?? null; } public function hasRequirement(string $key): bool { return \array_key_exists($key, $this->requirements); } /** * @return $this */ 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; } public function getCondition(): string { return $this->condition; } /** * @return $this */ public function setCondition(?string $condition): static { $this->condition = (string) $condition; $this->compiled = null; return $this; } /** * Compiles the route. * * @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(): CompiledRoute { if (null !== $this->compiled) { return $this->compiled; } $class = $this->getOption('compiler_class'); return $this->compiled = $class::compile($this); } private function extractInlineDefaultsAndRequirements(string $pattern): string { if (false === strpbrk($pattern, '?<')) { return $pattern; } 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 (str_ends_with($regex, '$')) { $regex = substr($regex, 0, -1); } elseif (\strlen($regex) - 2 === strpos($regex, '\\z')) { $regex = substr($regex, 0, -2); } if ('' === $regex) { throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" cannot be empty.', $key)); } 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']); } } README.md 0000644 00000002767 15025056712 0006042 0 ustar 00 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/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) RouteCompiler.php 0000644 00000034635 15025056712 0010064 0 ustar 00 <?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; /** * RouteCompiler compiles Route instances to CompiledRoute instances. * * @author Fabien Potencier <fabien@symfony.com> * @author Tobias Schultze <http://tobion.de> */ class RouteCompiler implements RouteCompilerInterface { /** * 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. */ public const SEPARATORS = '/,;.:-_~+*=@|'; /** * The maximum supported length of a PCRE subpattern name * http://pcre.org/current/doc/html/pcre2pattern.html#SEC16. * * @internal */ 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 */ public static function compile(Route $route): CompiledRoute { $hostVariables = []; $variables = []; $hostRegex = null; $hostTokens = []; if ('' !== $host = $route->getHost()) { $result = self::compilePattern($route, $host, true); $hostVariables = $result['variables']; $variables = $hostVariables; $hostTokens = $result['tokens']; $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); $staticPrefix = $result['staticPrefix']; $pathVariables = $result['variables']; foreach ($pathVariables as $pathParam) { if ('_fragment' === $pathParam) { throw new \InvalidArgumentException(sprintf('Route pattern "%s" cannot contain "_fragment" as a path parameter.', $route->getPath())); } } $variables = array_merge($variables, $pathVariables); $tokens = $result['tokens']; $regex = $result['regex']; return new CompiledRoute( $staticPrefix, $regex, $tokens, $pathVariables, $hostRegex, $hostTokens, $hostVariables, array_unique($variables) ); } private static function compilePattern(Route $route, string $pattern, bool $isHost): 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)) { 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)); } // 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); foreach ($matches as $match) { $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]); if (!\strlen($precedingText)) { $precedingChar = ''; } elseif ($useUtf8) { preg_match('/.$/u', $precedingText, $precedingChar); $precedingChar = $precedingChar[0]; } else { $precedingChar = substr($precedingText, -1); } $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)) { 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 %d characters in route pattern "%s". Please use a shorter name.', $varName, self::VARIABLE_MAXIMUM_LENGTH, $pattern)); } if ($isSeparator && $precedingText !== $precedingChar) { $tokens[] = ['text', substr($precedingText, 0, -\strlen($precedingChar))]; } elseif (!$isSeparator && '' !== $precedingText) { $tokens[] = ['text', $precedingText]; } $regexp = $route->getRequirement($varName); if (null === $regexp) { $followingPattern = (string) substr($pattern, $pos); // 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}', ['_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), $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 // quantifier. This prevents useless backtracking of PCRE and improves performance by 20% for matching those patterns. // Given the above example, there is no point in backtracking into {page} (that forbids the dot) when a dot must follow // after it. This optimization cannot be applied when the next char is no real separator or when the next variable is // directly adjacent, e.g. '/{x}{y}'. $regexp .= '+'; } } else { if (!preg_match('//u', $regexp)) { $useUtf8 = false; } elseif (!$needsUtf8 && preg_match('/[\x80-\xFF]|(?<!\\\\)\\\\(?:\\\\\\\\)*+(?-i:X|[pP][\{CLMNPSZ]|x\{[A-Fa-f0-9]{3})/', $regexp)) { 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); } 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[] = ['text', substr($pattern, $pos)]; } // find the first optional token $firstOptional = \PHP_INT_MAX; if (!$isHost) { for ($i = \count($tokens) - 1; $i >= 0; --$i) { $token = $tokens[$i]; // 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; } } } // compute the matching regexp $regexp = ''; for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) { $regexp .= self::computeRegexp($tokens, $i, $firstOptional); } $regexp = '{^'.$regexp.'$}sD'.($isHost ? 'i' : ''); // enable Utf8 matching if really required if ($needsUtf8) { $regexp .= 'u'; for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) { if ('variable' === $tokens[$i][0]) { $tokens[$i][4] = true; } } } return [ 'staticPrefix' => self::determineStaticPrefix($route, $tokens), 'regex' => $regexp, 'tokens' => array_reverse($tokens), 'variables' => $variables, ]; } /** * Determines the longest static prefix possible for a route. */ 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]; } $prefix = $tokens[0][1]; if (isset($tokens[1][1]) && '/' !== $tokens[1][1] && false === $route->hasDefault($tokens[1][3])) { $prefix .= $tokens[1][1]; } return $prefix; } /** * 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(string $pattern, bool $useUtf8): string { if ('' == $pattern) { // return empty string if pattern is empty or false (false which can be returned by substr) return ''; } // first remove all placeholders from the pattern so we can find the next real static character if ('' === $pattern = preg_replace('#\{\w+\}#', '', $pattern)) { return ''; } if ($useUtf8) { preg_match('/^./u', $pattern, $pattern); } return str_contains(static::SEPARATORS, $pattern[0]) ? $pattern[0] : ''; } /** * Computes the regexp used to match a specific token. It can be static text or a subpattern. * * @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 */ 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]); } 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]), $token[3], $token[2]); } else { $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); if ($nbTokens - 1 == $index) { // Close the optional subpatterns $regexp .= str_repeat(')?', $nbTokens - $firstOptional - (0 === $firstOptional ? 1 : 0)); } } return $regexp; } } } 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; } } RouterInterface.php 0000644 00000001767 15025056712 0010374 0 ustar 00 <?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\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; /** * RouterInterface is the interface that all Router classes must implement. * * This interface is the concatenation of UrlMatcherInterface and UrlGeneratorInterface. * * @author Fabien Potencier <fabien@symfony.com> */ interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface { /** * Gets the RouteCollection instance associated with this Router. * * 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(); } Loader/ContainerLoader.php 0000644 00000002016 15025056712 0011536 0 ustar 00 <?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); } } Loader/AnnotationDirectoryLoader.php 0000644 00000005127 15025056712 0013621 0 ustar 00 <?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\Resource\DirectoryResource; use Symfony\Component\Routing\RouteCollection; /** * AnnotationDirectoryLoader loads routing information from annotations set * on PHP classes and methods. * * @author Fabien Potencier <fabien@symfony.com> */ class AnnotationDirectoryLoader extends AnnotationFileLoader { /** * @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed */ public function load(mixed $path, string $type = null): ?RouteCollection { 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$/')); $files = iterator_to_array(new \RecursiveIteratorIterator( new \RecursiveCallbackFilterIterator( new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), function (\SplFileInfo $current) { return '.' !== substr($current->getBasename(), 0, 1); } ), \RecursiveIteratorIterator::LEAVES_ONLY )); usort($files, function (\SplFileInfo $a, \SplFileInfo $b) { return (string) $a > (string) $b ? 1 : -1; }); foreach ($files as $file) { if (!$file->isFile() || !str_ends_with($file->getFilename(), '.php')) { continue; } if ($class = $this->findClass($file)) { $refl = new \ReflectionClass($class); if ($refl->isAbstract()) { continue; } $collection->addCollection($this->loader->load($class, $type)); } } return $collection; } /** * {@inheritdoc} */ public function supports(mixed $resource, string $type = null): bool { if ('annotation' === $type) { return true; } if ($type || !\is_string($resource)) { return false; } try { return is_dir($this->locator->locate($resource)); } catch (\Exception $e) { return false; } } } Loader/ObjectLoader.php 0000644 00000005445 15025056712 0011033 0 ustar 00 <?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()); } } Loader/AnnotationFileLoader.php 0000644 00000010472 15025056712 0012533 0 ustar 00 <?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\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 * on a PHP class and its methods. * * @author Fabien Potencier <fabien@symfony.com> */ class AnnotationFileLoader extends FileLoader { protected $loader; public function __construct(FileLocatorInterface $locator, AnnotationClassLoader $loader) { if (!\function_exists('token_get_all')) { throw new \LogicException('The Tokenizer extension is required for the routing annotation loaders.'); } parent::__construct($locator); $this->loader = $loader; } /** * Loads from annotations from a file. * * @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed */ 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)); } gc_mem_caches(); return $collection; } /** * {@inheritdoc} */ public function supports(mixed $resource, string $type = null): bool { return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'annotation' === $type); } /** * Returns the full class name for the first class in the 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]) { 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]) { return $namespace.'\\'.$token[1]; } if (true === $namespace && isset($nsTokens[$token[0]])) { $namespace = $token[1]; 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 and anonymous classes $skipClassToken = false; for ($j = $i - 1; $j > 0; --$j) { if (!isset($tokens[$j][1])) { if ('(' === $tokens[$j] || ',' === $tokens[$j]) { $skipClassToken = true; } break; } if (\T_DOUBLE_COLON === $tokens[$j][0] || \T_NEW === $tokens[$j][0]) { $skipClassToken = true; break; } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) { break; } } if (!$skipClassToken) { $class = true; } } if (\T_NAMESPACE === $token[0]) { $namespace = true; } } return false; } } Loader/DirectoryLoader.php 0000644 00000003003 15025056712 0011555 0 ustar 00 <?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\Config\Resource\DirectoryResource; use Symfony\Component\Routing\RouteCollection; class DirectoryLoader extends FileLoader { /** * {@inheritdoc} */ public function load(mixed $file, string $type = null): mixed { $path = $this->locator->locate($file); $collection = new RouteCollection(); $collection->addResource(new DirectoryResource($path)); foreach (scandir($path) as $dir) { if ('.' !== $dir[0]) { $this->setCurrentDir($path); $subPath = $path.'/'.$dir; $subType = null; if (is_dir($subPath)) { $subPath .= '/'; $subType = 'directory'; } $subCollection = $this->import($subPath, $subType, false, $path); $collection->addCollection($subCollection); } } return $collection; } /** * {@inheritdoc} */ public function supports(mixed $resource, string $type = null): bool { // only when type is forced to directory, not to conflict with AnnotationLoader return 'directory' === $type; } } Loader/ClosureLoader.php 0000644 00000001672 15025056712 0011237 0 ustar 00 <?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\Routing\RouteCollection; /** * ClosureLoader loads routes from a PHP closure. * * The Closure must return a RouteCollection instance. * * @author Fabien Potencier <fabien@symfony.com> */ class ClosureLoader extends Loader { /** * Loads a Closure. */ public function load(mixed $closure, string $type = null): RouteCollection { return $closure($this->env); } /** * {@inheritdoc} */ public function supports(mixed $resource, string $type = null): bool { return $resource instanceof \Closure && (!$type || 'closure' === $type); } } Loader/YamlFileLoader.php 0000644 00000027366 15025056712 0011335 0 ustar 00 <?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\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\Yaml\Yaml; /** * YamlFileLoader loads Yaml routing files. * * @author Fabien Potencier <fabien@symfony.com> * @author Tobias Schultze <http://tobion.de> */ class YamlFileLoader extends FileLoader { 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; /** * @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid */ public function load(mixed $file, string $type = null): RouteCollection { $path = $this->locator->locate($file); if (!stream_is_local($path)) { throw new \InvalidArgumentException(sprintf('This is not a local file "%s".', $path)); } if (!file_exists($path)) { throw new \InvalidArgumentException(sprintf('File "%s" not found.', $path)); } $this->yamlParser ??= new YamlParser(); try { $parsedConfig = $this->yamlParser->parseFile($path, Yaml::PARSE_CONSTANT); } catch (ParseException $e) { throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML: ', $path).$e->getMessage(), 0, $e); } $collection = new RouteCollection(); $collection->addResource(new FileResource($path)); // empty file if (null === $parsedConfig) { return $collection; } // not an array 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'])) { $this->parseImport($collection, $config, $path, $file); } else { $this->parseRoute($collection, $name, $config, $path); } } return $collection; } /** * {@inheritdoc} */ public function supports(mixed $resource, string $type = null): bool { 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. */ protected function parseRoute(RouteCollection $collection, string $name, array $config, string $path) { if (isset($config['alias'])) { $alias = $collection->addAlias($name, $config['alias']); $deprecation = $config['deprecated'] ?? null; if (null !== $deprecation) { $alias->setDeprecated( $deprecation['package'], $deprecation['version'], $deprecation['message'] ?? '' ); } return; } $defaults = $config['defaults'] ?? []; $requirements = $config['requirements'] ?? []; $options = $config['options'] ?? []; foreach ($requirements as $placeholder => $requirement) { if (\is_int($placeholder)) { 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. */ protected function parseImport(RouteCollection $collection, array $config, string $path, string $file) { $type = $config['type'] ?? null; $prefix = $config['prefix'] ?? ''; $defaults = $config['defaults'] ?? []; $requirements = $config['requirements'] ?? []; $options = $config['options'] ?? []; $host = $config['host'] ?? null; $condition = $config['condition'] ?? null; $schemes = $config['schemes'] ?? null; $methods = $config['methods'] ?? null; $trailingSlashOnRoot = $config['trailing_slash_on_root'] ?? true; $namePrefix = $config['name_prefix'] ?? null; $exclude = $config['exclude'] ?? null; if (isset($config['controller'])) { $defaults['_controller'] = $config['controller']; } 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']; } $this->setCurrentDir(\dirname($path)); /** @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); } } /** * @throws \InvalidArgumentException If one of the provided config keys is not supported, * something is missing or the combination is nonsense */ protected function validate(mixed $config, string $name, string $path) { if (!\is_array($config)) { throw new \InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path)); } if (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)); } 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)); } 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)); } 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)); } } } } } Loader/PhpFileLoader.php 0000644 00000004234 15025056712 0011147 0 ustar 00 <?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\Config\Resource\FileResource; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Symfony\Component\Routing\RouteCollection; /** * PhpFileLoader loads routes from a PHP file. * * 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. */ public function load(mixed $file, string $type = null): RouteCollection { $path = $this->locator->locate($file); $this->setCurrentDir(\dirname($path)); // the closure forbids access to the private scope in the included file $loader = $this; $load = \Closure::bind(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->addResource(new FileResource($path)); return $collection; } /** * {@inheritdoc} */ public function supports(mixed $resource, string $type = null): bool { return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'php' === $type); } protected function callConfigurator(callable $result, string $path, string $file): RouteCollection { $collection = new RouteCollection(); $result(new RoutingConfigurator($collection, $this, $path, $file, $this->env)); return $collection; } } /** * @internal */ final class ProtectedPhpFileLoader extends PhpFileLoader { } Loader/XmlFileLoader.php 0000644 00000042175 15025056712 0011166 0 ustar 00 <?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\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. * * @author Fabien Potencier <fabien@symfony.com> * @author Tobias Schultze <http://tobion.de> */ class XmlFileLoader extends FileLoader { 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'; /** * @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(mixed $file, string $type = null): RouteCollection { $path = $this->locator->locate($file); $xml = $this->loadFile($path); $collection = new RouteCollection(); $collection->addResource(new FileResource($path)); // process routes and imports foreach ($xml->documentElement->childNodes as $node) { if (!$node instanceof \DOMElement) { continue; } $this->parseNode($collection, $node, $path, $file); } return $collection; } /** * Parses a node from a loaded XML file. * * @throws \InvalidArgumentException When the XML is invalid */ protected function parseNode(RouteCollection $collection, \DOMElement $node, string $path, string $file) { if (self::NAMESPACE_URI !== $node->namespaceURI) { return; } switch ($node->localName) { case 'route': $this->parseRoute($collection, $node, $path); break; 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)); } } /** * {@inheritdoc} */ public function supports(mixed $resource, string $type = null): bool { return \is_string($resource) && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'xml' === $type); } /** * Parses a route and adds it to the RouteCollection. * * @throws \InvalidArgumentException When the XML is invalid */ protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $path) { if ('' === $id = $node->getAttribute('id')) { throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" attribute.', $path)); } if ('' !== $alias = $node->getAttribute('alias')) { $alias = $collection->addAlias($id, $alias); if ($deprecationInfo = $this->parseDeprecation($node, $path)) { $alias->setDeprecated($deprecationInfo['package'], $deprecationInfo['version'], $deprecationInfo['message']); } return; } $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY); $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY); [$defaults, $requirements, $options, $condition, $paths, /* $prefixes */, $hosts] = $this->parseConfigs($node, $path); if (!$paths && '' === $node->getAttribute('path')) { throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have a "path" attribute or <path> child nodes.', $path)); } 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. * * @throws \InvalidArgumentException When the XML is invalid */ 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)); } $type = $node->getAttribute('type'); $prefix = $node->getAttribute('prefix'); $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; [$defaults, $requirements, $options, $condition, /* $paths */, $prefixes, $hosts] = $this->parseConfigs($node, $path); if ('' !== $prefix && $prefixes) { throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "prefix" attribute and <prefix> child nodes.', $path)); } $exclude = []; foreach ($node->childNodes as $child) { if ($child instanceof \DOMElement && $child->localName === $exclude && self::NAMESPACE_URI === $child->namespaceURI) { $exclude[] = $child->nodeValue; } } if ($node->hasAttribute('exclude')) { if ($exclude) { throw new \InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.'); } $exclude = [$node->getAttribute('exclude')]; } $this->setCurrentDir(\dirname($path)); /** @var RouteCollection[] $imported */ $imported = $this->import($resource, '' !== $type ? $type : null, false, $file, $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); } } /** * @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(string $file): \DOMDocument { return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH); } /** * Parses the config elements (default, requirement, option). * * @throws \InvalidArgumentException When the XML is invalid */ private function parseConfigs(\DOMElement $node, string $path): 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; } else { $defaults[$n->getAttribute('key')] = $this->parseDefaultsConfig($n, $path); } break; case 'requirement': $requirements[$n->getAttribute('key')] = trim($n->textContent); break; case 'option': $options[$n->getAttribute('key')] = XmlUtils::phpize(trim($n->textContent)); break; case 'condition': $condition = trim($n->textContent); break; default: throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement", "option" or "condition".', $n->localName, $path)); } } 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. */ private function parseDefaultsConfig(\DOMElement $element, string $path): array|bool|float|int|string|null { if ($this->isElementValueNull($element)) { return null; } // Check for existing element nodes in the default element. There can // only be a single element inside a default element. So this element // (if one was found) can safely be returned. foreach ($element->childNodes as $child) { if (!$child instanceof \DOMElement) { continue; } if (self::NAMESPACE_URI !== $child->namespaceURI) { continue; } return $this->parseDefaultNode($child, $path); } // If the default element doesn't contain a nested "bool", "int", "float", // "string", "list", or "map" element, the element contents will be treated // as the string value of the associated default option. return trim($element->textContent); } /** * Recursively parses the value of a "default" element. * * @throws \InvalidArgumentException when the XML is invalid */ private function parseDefaultNode(\DOMElement $node, string $path): array|bool|float|int|string|null { if ($this->isElementValueNull($node)) { return null; } switch ($node->localName) { case 'bool': return 'true' === trim($node->nodeValue) || '1' === trim($node->nodeValue); case 'int': return (int) trim($node->nodeValue); case 'float': return (float) trim($node->nodeValue); case 'string': return trim($node->nodeValue); case 'list': $list = []; foreach ($node->childNodes as $element) { if (!$element instanceof \DOMElement) { continue; } if (self::NAMESPACE_URI !== $element->namespaceURI) { continue; } $list[] = $this->parseDefaultNode($element, $path); } return $list; case 'map': $map = []; foreach ($node->childNodes as $element) { if (!$element instanceof \DOMElement) { continue; } if (self::NAMESPACE_URI !== $element->namespaceURI) { continue; } $map[$element->getAttribute('key')] = $this->parseDefaultNode($element, $path); } return $map; default: throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "bool", "int", "float", "string", "list", or "map".', $node->localName, $path)); } } private function isElementValueNull(\DOMElement $element): bool { $namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance'; if (!$element->hasAttributeNS($namespaceUri, 'nil')) { return false; } 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), ]; } } Loader/AnnotationClassLoader.php 0000644 00000032012 15025056712 0012713 0 ustar 00 <?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 Doctrine\Common\Annotations\Reader; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Loader\LoaderResolverInterface; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Routing\Annotation\Route as RouteAnnotation; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; /** * AnnotationClassLoader loads routing information from a PHP class and its methods. * * You need to define an implementation for the configureRoute() method. Most of the * time, this method should define some PHP callable to be called for the route * (a controller in MVC speak). * * The @Route annotation can be set on the class (for global parameters), * and on each method. * * The @Route annotation main value is the route path. The annotation also * 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") * * / * class Blog * { * /** * * @Route("/", name="blog_index") * * / * public function index() * { * } * /** * * @Route("/{id}", name="blog_post", requirements = {"id" = "\d+"}) * * / * public function show() * { * } * } * * On PHP 8, the annotation class can be used as an attribute as well: * #[Route('/Blog')] * class Blog * { * #[Route('/', name: 'blog_index')] * public function index() * { * } * #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])] * public function show() * { * } * } * * @author Fabien Potencier <fabien@symfony.com> * @author Alexander M. Turek <me@derrabus.de> */ abstract class AnnotationClassLoader implements LoaderInterface { protected $reader; protected $env; /** * @var string */ protected $routeAnnotationClass = RouteAnnotation::class; /** * @var int */ protected $defaultRouteIndex = 0; public function __construct(Reader $reader = null, string $env = null) { $this->reader = $reader; $this->env = $env; } /** * Sets the annotation class to read route properties from. */ public function setRouteAnnotationClass(string $class) { $this->routeAnnotationClass = $class; } /** * Loads from annotations from a class. * * @throws \InvalidArgumentException When route can't be parsed */ public function load(mixed $class, string $type = null): RouteCollection { if (!class_exists($class)) { throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); } $class = new \ReflectionClass($class); if ($class->isAbstract()) { throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class->getName())); } $globals = $this->getGlobals($class); $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->getAnnotations($method) as $annot) { $this->addRoute($collection, $annot, $globals, $class, $method); } } 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; } /** * @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; $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())); } } $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()); $host = $annot->getHost(); if (null === $host) { $host = $globals['host']; } $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; } 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; } } } 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(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); } /** * {@inheritdoc} */ public function setResolver(LoaderResolverInterface $resolver) { } /** * {@inheritdoc} */ public function getResolver(): LoaderResolverInterface { } /** * Gets the default route name for a class method. * * @return string */ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) { $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; } ++$this->defaultRouteIndex; return $name; } protected function getGlobals(\ReflectionClass $class) { $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 (null !== $annot->getPath()) { $globals['path'] = $annot->getPath(); } $globals['localized_paths'] = $annot->getLocalizedPaths(); if (null !== $annot->getRequirements()) { $globals['requirements'] = $annot->getRequirements(); } if (null !== $annot->getOptions()) { $globals['options'] = $annot->getOptions(); } if (null !== $annot->getDefaults()) { $globals['defaults'] = $annot->getDefaults(); } if (null !== $annot->getSchemes()) { $globals['schemes'] = $annot->getSchemes(); } if (null !== $annot->getMethods()) { $globals['methods'] = $annot->getMethods(); } if (null !== $annot->getHost()) { $globals['host'] = $annot->getHost(); } 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; } 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, 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; } } } } Loader/schema/routing/routing-1.0.xsd 0000644 00000017066 15025056712 0013421 0 ustar 00 <?xml version="1.0" encoding="UTF-8" ?> <xsd:schema xmlns="http://symfony.com/schema/routing" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://symfony.com/schema/routing" elementFormDefault="qualified"> <xsd:annotation> <xsd:documentation><![CDATA[ Symfony XML Routing Schema, version 1.0 Authors: Fabien Potencier, Tobias Schultze This scheme defines the elements and attributes that can be used to define routes. A route maps an HTTP request to a set of configuration variables. ]]></xsd:documentation> </xsd:annotation> <xsd:element name="routes" type="routes" /> <xsd:complexType name="routes"> <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" /> <xsd:element name="requirement" type="element" /> <xsd:element name="option" type="element" /> <xsd:element name="condition" type="xsd:string" /> </xsd:choice> </xsd:group> <xsd:complexType name="route"> <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" /> <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: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"> <xsd:choice minOccurs="0" maxOccurs="1"> <xsd:element name="bool" type="xsd:boolean" /> <xsd:element name="int" type="xsd:integer" /> <xsd:element name="float" type="xsd:float" /> <xsd:element name="string" type="xsd:string" /> <xsd:element name="list" type="list" /> <xsd:element name="map" type="map" /> </xsd:choice> <xsd:attribute name="key" type="xsd:string" use="required" /> </xsd:complexType> <xsd:complexType name="element"> <xsd:simpleContent> <xsd:extension base="xsd:string"> <xsd:attribute name="key" type="xsd:string" use="required" /> </xsd:extension> </xsd:simpleContent> </xsd:complexType> <xsd:complexType name="list"> <xsd:choice minOccurs="0" maxOccurs="unbounded"> <xsd:element name="bool" nillable="true" type="xsd:boolean" /> <xsd:element name="int" nillable="true" type="xsd:integer" /> <xsd:element name="float" nillable="true" type="xsd:float" /> <xsd:element name="string" nillable="true" type="xsd:string" /> <xsd:element name="list" nillable="true" type="list" /> <xsd:element name="map" nillable="true" type="map" /> </xsd:choice> </xsd:complexType> <xsd:complexType name="map"> <xsd:choice minOccurs="0" maxOccurs="unbounded"> <xsd:element name="bool" nillable="true" type="map-bool-entry" /> <xsd:element name="int" nillable="true" type="map-int-entry" /> <xsd:element name="float" nillable="true" type="map-float-entry" /> <xsd:element name="string" nillable="true" type="map-string-entry" /> <xsd:element name="list" nillable="true" type="map-list-entry" /> <xsd:element name="map" nillable="true" type="map-map-entry" /> </xsd:choice> </xsd:complexType> <xsd:complexType name="map-bool-entry"> <xsd:simpleContent> <xsd:extension base="xsd:boolean"> <xsd:attribute name="key" type="xsd:string" use="required" /> </xsd:extension> </xsd:simpleContent> </xsd:complexType> <xsd:complexType name="map-int-entry"> <xsd:simpleContent> <xsd:extension base="xsd:integer"> <xsd:attribute name="key" type="xsd:string" use="required" /> </xsd:extension> </xsd:simpleContent> </xsd:complexType> <xsd:complexType name="map-float-entry"> <xsd:simpleContent> <xsd:extension base="xsd:float"> <xsd:attribute name="key" type="xsd:string" use="required" /> </xsd:extension> </xsd:simpleContent> </xsd:complexType> <xsd:complexType name="map-string-entry"> <xsd:simpleContent> <xsd:extension base="xsd:string"> <xsd:attribute name="key" type="xsd:string" use="required" /> </xsd:extension> </xsd:simpleContent> </xsd:complexType> <xsd:complexType name="map-list-entry"> <xsd:complexContent> <xsd:extension base="list"> <xsd:attribute name="key" type="xsd:string" use="required" /> </xsd:extension> </xsd:complexContent> </xsd:complexType> <xsd:complexType name="map-map-entry"> <xsd:complexContent> <xsd:extension base="map"> <xsd:attribute name="key" type="xsd:string" use="required" /> </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> Loader/Configurator/CollectionConfigurator.php 0000644 00000007004 15025056712 0015607 0 ustar 00 <?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); } } Loader/Configurator/RouteConfigurator.php 0000644 00000002340 15025056712 0014610 0 ustar 00 <?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; } } Loader/Configurator/ImportConfigurator.php 0000644 00000003676 15025056712 0015001 0 ustar 00 <?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; } } Loader/Configurator/RoutingConfigurator.php 0000644 00000004256 15025056712 0015151 0 ustar 00 <?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; } } Loader/Configurator/Traits/HostTrait.php 0000644 00000003214 15025056712 0014317 0 ustar 00 <?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); } } } } Loader/Configurator/Traits/PrefixTrait.php 0000644 00000004553 15025056712 0014646 0 ustar 00 <?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, '/')); } } } } } Loader/Configurator/Traits/RouteTrait.php 0000644 00000007030 15025056712 0014500 0 ustar 00 <?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; } } Loader/Configurator/Traits/LocalizedRouteTrait.php 0000644 00000004762 15025056712 0016340 0 ustar 00 <?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); } } Loader/Configurator/Traits/AddTrait.php 0000644 00000003414 15025056712 0014074 0 ustar 00 <?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); } } Loader/Configurator/AliasConfigurator.php 0000644 00000002272 15025056712 0014547 0 ustar 00 <?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; } } Loader/GlobFileLoader.php 0000644 00000002072 15025056712 0011301 0 ustar 00 <?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; } } LICENSE 0000644 00000002051 15025056712 0005552 0 ustar 00 Copyright (c) 2004-2023 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 in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. CompiledRoute.php 0000644 00000010211 15025056712 0010026 0 ustar 00 <?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; /** * CompiledRoutes are returned by the RouteCompiler class. * * @author Fabien Potencier <fabien@symfony.com> */ class CompiledRoute implements \Serializable { private array $variables; private array $tokens; private string $staticPrefix; private string $regex; private array $pathVariables; private array $hostVariables; private ?string $hostRegex; private array $hostTokens; /** * @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 * @param array $pathVariables An array of path variables * @param string|null $hostRegex Host regex * @param array $hostTokens Host tokens * @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(string $staticPrefix, string $regex, array $tokens, array $pathVariables, string $hostRegex = null, array $hostTokens = [], array $hostVariables = [], array $variables = []) { $this->staticPrefix = $staticPrefix; $this->regex = $regex; $this->tokens = $tokens; $this->pathVariables = $pathVariables; $this->hostRegex = $hostRegex; $this->hostTokens = $hostTokens; $this->hostVariables = $hostVariables; $this->variables = $variables; } public function __serialize(): array { return [ 'vars' => $this->variables, 'path_prefix' => $this->staticPrefix, 'path_regex' => $this->regex, 'path_tokens' => $this->tokens, 'path_vars' => $this->pathVariables, 'host_regex' => $this->hostRegex, 'host_tokens' => $this->hostTokens, 'host_vars' => $this->hostVariables, ]; } /** * @internal */ final public function serialize(): string { 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']; $this->tokens = $data['path_tokens']; $this->pathVariables = $data['path_vars']; $this->hostRegex = $data['host_regex']; $this->hostTokens = $data['host_tokens']; $this->hostVariables = $data['host_vars']; } /** * @internal */ 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. */ public function getRegex(): string { return $this->regex; } /** * Returns the host regex. */ public function getHostRegex(): ?string { return $this->hostRegex; } /** * Returns the tokens. */ public function getTokens(): array { return $this->tokens; } /** * Returns the host tokens. */ public function getHostTokens(): array { return $this->hostTokens; } /** * Returns the variables. */ public function getVariables(): array { return $this->variables; } /** * Returns the path variables. */ public function getPathVariables(): array { return $this->pathVariables; } /** * Returns the host variables. */ public function getHostVariables(): array { return $this->hostVariables; } } Matcher/CompiledUrlMatcher.php 0000644 00000001535 15025056712 0012372 0 ustar 00 <?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; } } Matcher/RedirectableUrlMatcherInterface.php 0000644 00000001537 15025056712 0015046 0 ustar 00 <?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; /** * RedirectableUrlMatcherInterface knows how to redirect the user. * * @author Fabien Potencier <fabien@symfony.com> */ interface RedirectableUrlMatcherInterface { /** * 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) */ public function redirect(string $path, string $route, string $scheme = null): array; } Matcher/UrlMatcher.php 0000644 00000021734 15025056712 0010720 0 ustar 00 <?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\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\RequestContext; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; /** * UrlMatcher matches URL based on a set of routes. * * @author Fabien Potencier <fabien@symfony.com> */ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface { public const REQUIREMENT_MATCH = 0; public const REQUIREMENT_MISMATCH = 1; public const ROUTE_MATCH = 2; /** @var RequestContext */ protected $context; /** * Collects HTTP methods that would be allowed for the request. */ protected $allow = []; /** * Collects URI schemes that would be allowed for the request. * * @internal */ protected array $allowSchemes = []; protected $routes; protected $request; protected $expressionLanguage; /** * @var ExpressionFunctionProviderInterface[] */ protected $expressionLanguageProviders = []; public function __construct(RouteCollection $routes, RequestContext $context) { $this->routes = $routes; $this->context = $context; } /** * {@inheritdoc} */ public function setContext(RequestContext $context) { $this->context = $context; } /** * {@inheritdoc} */ public function getContext(): RequestContext { return $this->context; } /** * {@inheritdoc} */ public function match(string $pathinfo): array { $this->allow = $this->allowSchemes = []; if ($ret = $this->matchCollection(rawurldecode($pathinfo) ?: '/', $this->routes)) { return $ret; } 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): array { $this->request = $request; $ret = $this->match($request->getPathInfo()); $this->request = null; return $ret; } public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) { $this->expressionLanguageProviders[] = $provider; } /** * Tries to match a URL with a set of routes. * * @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(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 ('' !== $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; } $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)) { continue; } $status = $this->handleRouteRequirements($pathinfo, $name, $route); if (self::REQUIREMENT_MISMATCH === $status[0]) { continue; } 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 []; } /** * Returns an array of values to use as request attributes. * * 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). */ 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, $defaults); } /** * Handles specific route requirements. * * @return array The first element represents the status, the second contains additional information */ protected function handleRouteRequirements(string $pathinfo, string $name, Route $route): array { // expression condition if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) { return [self::REQUIREMENT_MISMATCH, null]; } return [self::REQUIREMENT_MATCH, null]; } /** * Get merged default parameters. */ protected function mergeDefaults(array $params, array $defaults): array { foreach ($params as $key => $value) { if (!\is_int($key) && null !== $value) { $defaults[$key] = $value; } } return $defaults; } protected function getExpressionLanguage() { if (null === $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; } /** * @internal */ protected function createRequest(string $pathinfo): ?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(), [], [], [ 'SCRIPT_FILENAME' => $this->context->getBaseUrl(), 'SCRIPT_NAME' => $this->context->getBaseUrl(), ]); } } Matcher/ExpressionLanguageProvider.php 0000644 00000003247 15025056712 0014167 0 ustar 00 <?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); } } Matcher/RequestMatcherInterface.php 0000644 00000002341 15025056712 0013420 0 ustar 00 <?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\HttpFoundation\Request; 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. * * @author Fabien Potencier <fabien@symfony.com> */ interface RequestMatcherInterface { /** * Tries to match a request with a set of routes. * * If the matcher cannot find information, it must throw one of the exceptions documented * below. * * @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): array; } Matcher/UrlMatcherInterface.php 0000644 00000002532 15025056712 0012534 0 ustar 00 <?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\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. * * @author Fabien Potencier <fabien@symfony.com> */ interface UrlMatcherInterface extends RequestContextAwareInterface { /** * Tries to match a URL path with a set of routes. * * 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) * * @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(string $pathinfo): array; } Matcher/RedirectableUrlMatcher.php 0000644 00000004071 15025056712 0013221 0 ustar 00 <?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\Exception\ExceptionInterface; use Symfony\Component\Routing\Exception\ResourceNotFoundException; /** * @author Fabien Potencier <fabien@symfony.com> */ abstract class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface { /** * {@inheritdoc} */ public function match(string $pathinfo): array { try { return parent::match($pathinfo); } catch (ResourceNotFoundException $e) { if (!\in_array($this->context->getMethod(), ['HEAD', 'GET'], true)) { throw $e; } if ($this->allowSchemes) { redirect_scheme: $scheme = $this->context->getScheme(); $this->context->setScheme(current($this->allowSchemes)); try { $ret = parent::match($pathinfo); 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; } } } } } Matcher/Dumper/StaticPrefixCollection.php 0000644 00000015621 15025056712 0014525 0 ustar 00 <?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\RouteCollection; /** * Prefix tree of routes preserving routes order. * * @author Frank de Jonge <info@frankdejonge.nl> * @author Nicolas Grekas <p@tchwork.com> * * @internal */ class StaticPrefixCollection { private string $prefix; /** * @var string[] */ private array $staticPrefixes = []; /** * @var string[] */ private array $prefixes = []; /** * @var array[]|self[] */ private array $items = []; public function __construct(string $prefix = '/') { $this->prefix = $prefix; } public function getPrefix(): string { return $this->prefix; } /** * @return array[]|self[] */ public function getRoutes(): array { return $this->items; } /** * Adds a route to a group. */ public function addRoute(string $prefix, array|StaticPrefixCollection $route) { [$prefix, $staticPrefix] = $this->getCommonPrefix($prefix, $prefix); for ($i = \count($this->items) - 1; 0 <= $i; --$i) { $item = $this->items[$i]; [$commonPrefix, $commonStaticPrefix] = $this->getCommonPrefix($prefix, $this->prefixes[$i]); if ($this->prefix === $commonPrefix) { // the new route and a previous one have no common prefix, let's see if they are exclusive to each others 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; } continue; } 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]; $this->staticPrefixes[$i] = $commonStaticPrefix; $this->prefixes[$i] = $commonPrefix; $this->items[$i] = $child; } return; } // No optimised case was found, in this case we simple add the route for possible // grouping when new routes are added. $this->staticPrefixes[] = $staticPrefix; $this->prefixes[] = $prefix; $this->items[] = $route; } /** * Linearizes back a set of nested routes into a collection. */ public function populateCollection(RouteCollection $routes): RouteCollection { foreach ($this->items as $route) { if ($route instanceof self) { $route->populateCollection($routes); } else { $routes->add(...$route); } } return $routes; } /** * Gets the full and static common prefixes between two route patterns. * * The static prefix stops at last at the first opening bracket. */ private function getCommonPrefix(string $prefix, string $anotherPrefix): array { $baseLength = \strlen($this->prefix); $end = min(\strlen($prefix), \strlen($anotherPrefix)); $staticLength = null; set_error_handler([__CLASS__, 'handleError']); 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)]; } public static function handleError(int $type, string $msg) { return str_contains($msg, 'Compilation failed: lookbehind assertion is not fixed length'); } } Matcher/Dumper/CompiledUrlMatcherTrait.php 0000644 00000016725 15025056712 0014641 0 ustar 00 <?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 []; } } Matcher/Dumper/MatcherDumper.php 0000644 00000001430 15025056712 0012635 0 ustar 00 <?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\RouteCollection; /** * MatcherDumper is the abstract class for all built-in matcher dumpers. * * @author Fabien Potencier <fabien@symfony.com> */ abstract class MatcherDumper implements MatcherDumperInterface { private $routes; public function __construct(RouteCollection $routes) { $this->routes = $routes; } /** * {@inheritdoc} */ public function getRoutes(): RouteCollection { return $this->routes; } } Matcher/Dumper/CompiledUrlMatcherDumper.php 0000644 00000045203 15025056712 0015003 0 ustar 00 <?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); } } Matcher/Dumper/MatcherDumperInterface.php 0000644 00000001512 15025056712 0014457 0 ustar 00 <?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\RouteCollection; /** * MatcherDumperInterface is the interface that all matcher dumper classes must implement. * * @author Fabien Potencier <fabien@symfony.com> */ 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. */ public function dump(array $options = []): string; /** * Gets the routes to dump. */ public function getRoutes(): RouteCollection; } Matcher/TraceableUrlMatcher.php 0000644 00000015261 15025056712 0012521 0 ustar 00 <?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\HttpFoundation\Request; use Symfony\Component\Routing\Exception\ExceptionInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; /** * TraceableUrlMatcher helps debug path info matching by tracing the match. * * @author Fabien Potencier <fabien@symfony.com> */ class TraceableUrlMatcher extends UrlMatcher { public const ROUTE_DOES_NOT_MATCH = 0; public const ROUTE_ALMOST_MATCHES = 1; public const ROUTE_MATCHES = 2; protected $traces; public function getTraces(string $pathinfo) { $this->traces = []; try { $this->match($pathinfo); } catch (ExceptionInterface $e) { } return $this->traces; } public function getTracesForRequest(Request $request) { $this->request = $request; $traces = $this->getTraces($request->getPathInfo()); $this->request = null; return $traces; } 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 ('' !== $staticPrefix && !str_starts_with($trimmedPathinfo, $staticPrefix)) { $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route); continue; } $regex = $compiledRoute->getRegex(); $pos = strrpos($regex, '$'); $hasTrailingSlash = '/' === $regex[$pos - 1]; $regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash); if (!preg_match($regex, $pathinfo, $matches)) { // does it match without any requirements? $r = new Route($route->getPath(), $route->getDefaults(), [], $route->getOptions()); $cr = $r->compile(); if (!preg_match($cr->getRegex(), $pathinfo)) { $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route); continue; } foreach ($route->getRequirements() as $n => $regex) { $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)) { $this->addTrace(sprintf('Requirement for "%s" does not match (%s)', $n, $regex), self::ROUTE_ALMOST_MATCHES, $name, $route); continue 2; } } 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 = []; 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; } $status = $this->handleRouteRequirements($pathinfo, $name, $route); 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; } if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) { $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route); return $this->allow = $this->allowSchemes = []; } $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route); continue; } 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 ($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 $this->getAttributes($route, $name, array_replace($matches, $hostMatches, $status[1] ?? [])); } return []; } private function addTrace(string $log, int $level = self::ROUTE_DOES_NOT_MATCH, string $name = null, Route $route = null) { $this->traces[] = [ 'log' => $log, 'name' => $name, 'level' => $level, 'path' => null !== $route ? $route->getPath() : null, ]; } } Generator/CompiledUrlGenerator.php 0000644 00000004761 15025056712 0013304 0 ustar 00 <?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); } } Generator/UrlGenerator.php 0000644 00000036333 15025056712 0011627 0 ustar 00 <?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\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 * based on the passed parameters. * * @author Fabien Potencier <fabien@symfony.com> * @author Tobias Schultze <http://tobion.de> */ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInterface { 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' => '*', ]; protected $routes; protected $context; /** * @var bool|null */ protected $strictRequirements = true; 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. * * PHP's rawurlencode() encodes all chars except "a-zA-Z0-9-._~" according to RFC 3986. But we want to allow some chars * to be used in their literal form (reasons below). Other chars inside the path must of course be encoded, e.g. * "?" and "#" (would be interpreted wrongly as query and fragment identifier), * "'" and """ (are used as delimiters in HTML). */ protected $decodedChars = [ // 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' => '@', '%3A' => ':', // these chars are only sub-delimiters that have no predefined meaning and can therefore be used literally // so URI producing applications can use these chars to delimit subcomponents in a path segment without being encoded for better readability '%3B' => ';', '%2C' => ',', '%3D' => '=', '%2B' => '+', '%21' => '!', '%2A' => '*', '%7C' => '|', ]; 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; } /** * {@inheritdoc} */ public function setContext(RequestContext $context) { $this->context = $context; } /** * {@inheritdoc} */ public function getContext(): RequestContext { return $this->context; } /** * {@inheritdoc} */ public function setStrictRequirements(?bool $enabled) { $this->strictRequirements = $enabled; } /** * {@inheritdoc} */ public function isStrictRequirements(): ?bool { return $this->strictRequirements; } /** * {@inheritdoc} */ public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { $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(); $defaults = $route->getDefaults(); $variables = $compiledRoute->getVariables(); if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) { if (!\in_array('_locale', $variables, true)) { unset($parameters['_locale']); } elseif (!isset($parameters['_locale'])) { $parameters['_locale'] = $defaults['_locale']; } } return $this->doGenerate($variables, $defaults, $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens(), $route->getSchemes()); } /** * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route * @throws InvalidParameterException When a parameter value for a placeholder is not correct because * it does not match the requirement */ 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); // all params must be given if ($diff = array_diff_key($variables, $mergedParams)) { throw new MissingMandatoryParametersException(sprintf('Some mandatory parameters are missing ("%s") to generate a URL for route "%s".', implode('", "', array_keys($diff)), $name)); } $url = ''; $optional = true; $message = 'Parameter "{parameter}" for route "{route}" must match "{expected}" ("{given}" given) to generate a corresponding URL.'; foreach ($tokens as $token) { if ('variable' === $token[0]) { $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, ['{parameter}' => $varName, '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$varName]])); } if ($this->logger) { $this->logger->error($message, ['parameter' => $varName, 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$varName]]); } return ''; } $url = $token[1].$mergedParams[$varName].$url; $optional = false; } } else { // static text $url = $token[1].$url; $optional = false; } } if ('' === $url) { $url = '/'; } // the contexts base URL is already encoded (see Symfony\Component\HttpFoundation\Request) $url = strtr(rawurlencode($url), $this->decodedChars); // the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3 // so we need to encode them as they are not used for this purpose here // otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route $url = strtr($url, ['/../' => '/%2E%2E/', '/./' => '/%2E/']); if (str_ends_with($url, '/..')) { $url = substr($url, 0, -2).'%2E%2E'; } elseif (str_ends_with($url, '/.')) { $url = substr($url, 0, -1).'%2E'; } $schemeAuthority = ''; $host = $this->context->getHost(); $scheme = $this->context->getScheme(); 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]) { // 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]]])); } if ($this->logger) { $this->logger->error($message, ['parameter' => $token[3], 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$token[3]]]); } return ''; } $routeHost = $token[1].$mergedParams[$token[3]].$routeHost; } else { $routeHost = $token[1].$routeHost; } } 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()) { $port = ':'.$this->context->getHttpPort(); } elseif ('https' === $scheme && 443 !== $this->context->getHttpsPort()) { $port = ':'.$this->context->getHttpsPort(); } $schemeAuthority = self::NETWORK_PATH === $referenceType || '' === $scheme ? '//' : "$scheme://"; $schemeAuthority .= $host.$port; } } if (self::RELATIVE_PATH === $referenceType) { $url = self::getRelativePath($this->context->getPathInfo(), $url); } else { $url = $schemeAuthority.$this->context->getBaseUrl().$url; } // add a query string if needed $extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, function ($a, $b) { return $a == $b ? 0 : 1; }); array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) { if (\is_object($v)) { if ($vars = get_object_vars($v)) { array_walk_recursive($vars, $caster); $v = $vars; } elseif (method_exists($v, '__toString')) { $v = (string) $v; } } }); // extract fragment $fragment = $defaults['_fragment'] ?? ''; if (isset($extra['_fragment'])) { $fragment = $extra['_fragment']; unset($extra['_fragment']); } if ($extra && $query = http_build_query($extra, '', '&', \PHP_QUERY_RFC3986)) { $url .= '?'.strtr($query, self::QUERY_FRAGMENT_DECODED); } if ('' !== $fragment) { $url .= '#'.strtr(rawurlencode($fragment), self::QUERY_FRAGMENT_DECODED); } return $url; } /** * Returns the target path as relative reference from the base path. * * Only the URIs path component (no schema, host etc.) is relevant and must be given, starting with a slash. * Both paths must be absolute and not contain relative parts. * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives. * Furthermore, they can be used to reduce the link size in documents. * * Example target paths, given a base path of "/a/b/c/d": * - "/a/b/c/d" -> "" * - "/a/b/c/" -> "./" * - "/a/b/" -> "../" * - "/a/b/c/other" -> "other" * - "/a/x/y" -> "../../x/y" * * @param string $basePath The base path * @param string $targetPath The target path */ public static function getRelativePath(string $basePath, string $targetPath): string { if ($basePath === $targetPath) { return ''; } $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath); $targetDirs = explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? substr($targetPath, 1) : $targetPath); array_pop($sourceDirs); $targetFile = array_pop($targetDirs); foreach ($sourceDirs as $i => $dir) { if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) { unset($sourceDirs[$i], $targetDirs[$i]); } else { break; } } $targetDirs[] = $targetFile; $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 // as the first segment of a relative-path reference, as it would be mistaken for a scheme name // (see http://tools.ietf.org/html/rfc3986#section-4.2). return '' === $path || '/' === $path[0] || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos) ? "./$path" : $path; } } Generator/UrlGeneratorInterface.php 0000644 00000006614 15025056712 0013447 0 ustar 00 <?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 Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\RequestContextAwareInterface; /** * UrlGeneratorInterface is the interface that all URL generator classes must implement. * * The constants in this interface define the different types of resource references that * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986 * We are using the term "URL" instead of "URI" as this is more common in web applications * and we do not need to distinguish them as the difference is mostly semantical and * less technical. Generating URIs, i.e. representation-independent resource identifiers, * is also possible. * * @author Fabien Potencier <fabien@symfony.com> * @author Tobias Schultze <http://tobion.de> */ interface UrlGeneratorInterface extends RequestContextAwareInterface { /** * Generates an absolute URL, e.g. "http://example.com/dir/file". */ public const ABSOLUTE_URL = 0; /** * Generates an absolute path, e.g. "/dir/file". */ public const ABSOLUTE_PATH = 1; /** * Generates a relative path based on the current request path, e.g. "../parent-file". * * @see UrlGenerator::getRelativePath() */ 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. */ public const NETWORK_PATH = 3; /** * Generates a URL or path for a specific route based on the given parameters. * * Parameters that reference placeholders in the route pattern will substitute them in the * path or host. Extra params are added as query string to the URL. * * When the passed reference type cannot be generated for the route because it requires a different * host or scheme than the current one, the method will return a more comprehensive reference * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH * but the route requires the https scheme whereas the current scheme is http, it will instead return an * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches * the route in any case. * * If there is no route with the given name, the generator must throw the RouteNotFoundException. * * The special parameter _fragment will be used as the document fragment suffixed to the final 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(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string; } Generator/Dumper/GeneratorDumper.php 0000644 00000001436 15025056712 0013551 0 ustar 00 <?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\RouteCollection; /** * GeneratorDumper is the base class for all built-in generator dumpers. * * @author Fabien Potencier <fabien@symfony.com> */ abstract class GeneratorDumper implements GeneratorDumperInterface { private $routes; public function __construct(RouteCollection $routes) { $this->routes = $routes; } /** * {@inheritdoc} */ public function getRoutes(): RouteCollection { return $this->routes; } } Generator/Dumper/GeneratorDumperInterface.php 0000644 00000001514 15025056712 0015367 0 ustar 00 <?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\RouteCollection; /** * GeneratorDumperInterface is the interface that all generator dumper classes must implement. * * @author Fabien Potencier <fabien@symfony.com> */ 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. */ public function dump(array $options = []): string; /** * Gets the routes to dump. */ public function getRoutes(): RouteCollection; } Generator/Dumper/CompiledUrlGeneratorDumper.php 0000644 00000007630 15025056712 0015713 0 ustar 00 <?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; } } Generator/ConfigurableRequirementsInterface.php 0000644 00000004244 15025056712 0016037 0 ustar 00 <?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; /** * ConfigurableRequirementsInterface must be implemented by URL generators that * can be configured whether an exception should be generated when the parameters * do not match the requirements. It is also possible to disable the requirements * check for URL generation completely. * * 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 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. * - setStrictRequirements(null): Return the URL with the given parameters without * checking the requirements at all. When generating a URL you should either trust * your params or you validated them beforehand because otherwise it would break your * link anyway. So in production environment you should know that params always pass * the requirements. Thus this option allows to disable the check on URL generation for * performance reasons (saving a preg_match for each requirement every time a URL is * generated). * * @author Fabien Potencier <fabien@symfony.com> * @author Tobias Schultze <http://tobion.de> */ interface ConfigurableRequirementsInterface { /** * Enables or disables the exception on incorrect parameters. * Passing null will deactivate the requirements check completely. */ public function setStrictRequirements(?bool $enabled); /** * Returns whether to throw an exception on incorrect parameters. * Null means the requirements check is deactivated completely. */ public function isStrictRequirements(): ?bool; } Router.php 0000644 00000027441 15025056712 0006550 0 ustar 00 <?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 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\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; /** * The Router class is an example of the integration of all pieces of the * routing system for easier use. * * @author Fabien Potencier <fabien@symfony.com> */ class Router implements RouterInterface, RequestMatcherInterface { /** * @var UrlMatcherInterface|null */ protected $matcher; /** * @var UrlGeneratorInterface|null */ protected $generator; /** * @var RequestContext */ protected $context; /** * @var LoaderInterface */ protected $loader; /** * @var RouteCollection|null */ protected $collection; /** * @var mixed */ protected $resource; /** * @var array */ protected $options = []; /** * @var LoggerInterface|null */ protected $logger; /** * @var string|null */ protected $defaultLocale; private $configCacheFactory; /** * @var ExpressionFunctionProviderInterface[] */ private array $expressionLanguageProviders = []; 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->setOptions($options); $this->defaultLocale = $defaultLocale; } /** * Sets options. * * Available options: * * * 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_dumper_class: The name of a GeneratorDumperInterface implementation * * matcher_class: The name of a UrlMatcherInterface 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) * * @throws \InvalidArgumentException When unsupported option is provided */ public function setOptions(array $options) { $this->options = [ 'cache_dir' => null, 'debug' => false, '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 = []; foreach ($options as $key => $value) { if (\array_key_exists($key, $this->options)) { $this->options[$key] = $value; } else { $invalid[] = $key; } } if ($invalid) { throw new \InvalidArgumentException(sprintf('The Router does not support the following options: "%s".', implode('", "', $invalid))); } } /** * Sets an option. * * @throws \InvalidArgumentException */ public function setOption(string $key, mixed $value) { if (!\array_key_exists($key, $this->options)) { throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key)); } $this->options[$key] = $value; } /** * Gets an option value. * * @throws \InvalidArgumentException */ public function getOption(string $key): mixed { if (!\array_key_exists($key, $this->options)) { throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key)); } return $this->options[$key]; } /** * {@inheritdoc} */ public function getRouteCollection() { if (null === $this->collection) { $this->collection = $this->loader->load($this->resource, $this->options['resource_type']); } return $this->collection; } /** * {@inheritdoc} */ public function setContext(RequestContext $context) { $this->context = $context; if (null !== $this->matcher) { $this->getMatcher()->setContext($context); } if (null !== $this->generator) { $this->getGenerator()->setContext($context); } } /** * {@inheritdoc} */ public function getContext(): RequestContext { return $this->context; } /** * Sets the ConfigCache factory to use. */ public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory) { $this->configCacheFactory = $configCacheFactory; } /** * {@inheritdoc} */ public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { return $this->getGenerator()->generate($name, $parameters, $referenceType); } /** * {@inheritdoc} */ public function match(string $pathinfo): array { return $this->getMatcher()->match($pathinfo); } /** * {@inheritdoc} */ public function matchRequest(Request $request): array { $matcher = $this->getMatcher(); if (!$matcher instanceof RequestMatcherInterface) { // fallback to the default UrlMatcherInterface return $matcher->match($request->getPathInfo()); } return $matcher->matchRequest($request); } /** * Gets the UrlMatcher or RequestMatcher instance associated with this Router. */ public function getMatcher(): UrlMatcherInterface|RequestMatcherInterface { if (null !== $this->matcher) { return $this->matcher; } 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); } } return $this->matcher; } $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/url_matching_routes.php', function (ConfigCacheInterface $cache) { $dumper = $this->getMatcherDumperInstance(); if (method_exists($dumper, 'addExpressionLanguageProvider')) { foreach ($this->expressionLanguageProviders as $provider) { $dumper->addExpressionLanguageProvider($provider); } } $cache->write($dumper->dump(), $this->getRouteCollection()->getResources()); } ); return $this->matcher = new $this->options['matcher_class'](self::getCompiledRoutes($cache->getPath()), $this->context); } /** * Gets the UrlGenerator instance associated with this Router. */ public function getGenerator(): UrlGeneratorInterface { if (null !== $this->generator) { return $this->generator; } 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'].'/url_generating_routes.php', function (ConfigCacheInterface $cache) { $dumper = $this->getGeneratorDumperInstance(); $cache->write($dumper->dump(), $this->getRouteCollection()->getResources()); } ); $this->generator = new $this->options['generator_class'](self::getCompiledRoutes($cache->getPath()), $this->context, $this->logger, $this->defaultLocale); } if ($this->generator instanceof ConfigurableRequirementsInterface) { $this->generator->setStrictRequirements($this->options['strict_requirements']); } return $this->generator; } public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) { $this->expressionLanguageProviders[] = $provider; } protected function getGeneratorDumperInstance(): GeneratorDumperInterface { return new $this->options['generator_dumper_class']($this->getRouteCollection()); } protected function getMatcherDumperInstance(): MatcherDumperInterface { return new $this->options['matcher_dumper_class']($this->getRouteCollection()); } /** * Provides the ConfigCache factory implementation, falling back to a * default implementation if necessary. */ private function getConfigCacheFactory(): ConfigCacheFactoryInterface { 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; } if (null === self::$cache) { return require $path; } return self::$cache[$path] ??= require $path; } }
| ver. 1.4 |
.
| PHP 8.1.32 | Generation time: 0 |
proxy
|
phpinfo
|
Settings