diff --git a/Annotation/Route.php b/Annotation/Route.php index dda3bdad..1a85a703 100644 --- a/Annotation/Route.php +++ b/Annotation/Route.php @@ -16,6 +16,9 @@ class_exists(\Symfony\Component\Routing\Attribute\Route::class); if (false) { + /** + * @deprecated since Symfony 7.4, use {@see \Symfony\Component\Routing\Attribute\Route} instead + */ #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class Route extends \Symfony\Component\Routing\Attribute\Route { diff --git a/Attribute/DeprecatedAlias.php b/Attribute/DeprecatedAlias.php index ae5a6821..5861f2a2 100644 --- a/Attribute/DeprecatedAlias.php +++ b/Attribute/DeprecatedAlias.php @@ -17,28 +17,32 @@ class DeprecatedAlias { public function __construct( - private string $aliasName, - private string $package, - private string $version, - private string $message = '', + public readonly string $aliasName, + public readonly string $package, + public readonly string $version, + public readonly string $message = '', ) { } + #[\Deprecated('Use the "message" property instead', 'symfony/routing:7.4')] public function getMessage(): string { return $this->message; } + #[\Deprecated('Use the "aliasName" property instead', 'symfony/routing:7.4')] public function getAliasName(): string { return $this->aliasName; } + #[\Deprecated('Use the "package" property instead', 'symfony/routing:7.4')] public function getPackage(): string { return $this->package; } + #[\Deprecated('Use the "version" property instead', 'symfony/routing:7.4')] public function getVersion(): string { return $this->version; diff --git a/Attribute/Route.php b/Attribute/Route.php index 003bbe64..172b621a 100644 --- a/Attribute/Route.php +++ b/Attribute/Route.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Routing\Attribute; +use Symfony\Component\Routing\Exception\LogicException; + /** * @author Fabien Potencier * @author Alexander M. Turek @@ -18,14 +20,17 @@ #[\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; - /** - * @var (string|DeprecatedAlias)[] - */ - private array $aliases = []; + /** @var string[] */ + public array $methods; + + /** @var string[] */ + public array $envs; + + /** @var string[] */ + public array $schemes; + + /** @var (string|DeprecatedAlias)[] */ + public array $aliases = []; /** * @param string|array|null $path The route path (i.e. "/user/login") @@ -42,35 +47,32 @@ class Route * @param string|null $format The format returned by the route (i.e. "json", "xml") * @param bool|null $utf8 Whether the route accepts UTF-8 in its parameters * @param bool|null $stateless Whether the route is defined as stateless or stateful, @see https://symfony.com/doc/current/routing.html#stateless-routes - * @param string|null $env The env in which the route is defined (i.e. "dev", "test", "prod") + * @param string|string[]|null $env The env(s) in which the route is defined (i.e. "dev", "test", "prod", ["dev", "test"]) * @param string|DeprecatedAlias|(string|DeprecatedAlias)[] $alias The list of aliases for this route */ public function __construct( - string|array|null $path = null, - private ?string $name = null, - private array $requirements = [], - private array $options = [], - private array $defaults = [], - private ?string $host = null, + public string|array|null $path = null, + public ?string $name = null, + public array $requirements = [], + public array $options = [], + public array $defaults = [], + public ?string $host = null, array|string $methods = [], array|string $schemes = [], - private ?string $condition = null, - private ?int $priority = null, + public ?string $condition = null, + public ?int $priority = null, ?string $locale = null, ?string $format = null, ?bool $utf8 = null, ?bool $stateless = null, - private ?string $env = null, + string|array|null $env = null, string|DeprecatedAlias|array $alias = [], ) { - if (\is_array($path)) { - $this->localizedPaths = $path; - } else { - $this->path = $path; - } - $this->setMethods($methods); - $this->setSchemes($schemes); - $this->setAliases($alias); + $this->path = $path; + $this->methods = (array) $methods; + $this->schemes = (array) $schemes; + $this->envs = (array) $env; + $this->aliases = \is_array($alias) ? $alias : [$alias]; if (null !== $locale) { $this->defaults['_locale'] = $locale; @@ -89,129 +91,161 @@ public function __construct( } } + #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] public function setPath(string $path): void { $this->path = $path; } + #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] public function getPath(): ?string { - return $this->path; + return \is_array($this->path) ? null : $this->path; } + #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] public function setLocalizedPaths(array $localizedPaths): void { - $this->localizedPaths = $localizedPaths; + $this->path = $localizedPaths; } + #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] public function getLocalizedPaths(): array { - return $this->localizedPaths; + return \is_array($this->path) ? $this->path : []; } + #[\Deprecated('Use the "host" property instead', 'symfony/routing:7.4')] public function setHost(string $pattern): void { $this->host = $pattern; } + #[\Deprecated('Use the "host" property instead', 'symfony/routing:7.4')] public function getHost(): ?string { return $this->host; } + #[\Deprecated('Use the "name" property instead', 'symfony/routing:7.4')] public function setName(string $name): void { $this->name = $name; } + #[\Deprecated('Use the "name" property instead', 'symfony/routing:7.4')] public function getName(): ?string { return $this->name; } + #[\Deprecated('Use the "requirements" property instead', 'symfony/routing:7.4')] public function setRequirements(array $requirements): void { $this->requirements = $requirements; } + #[\Deprecated('Use the "requirements" property instead', 'symfony/routing:7.4')] public function getRequirements(): array { return $this->requirements; } + #[\Deprecated('Use the "options" property instead', 'symfony/routing:7.4')] public function setOptions(array $options): void { $this->options = $options; } + #[\Deprecated('Use the "options" property instead', 'symfony/routing:7.4')] public function getOptions(): array { return $this->options; } + #[\Deprecated('Use the "defaults" property instead', 'symfony/routing:7.4')] public function setDefaults(array $defaults): void { $this->defaults = $defaults; } + #[\Deprecated('Use the "defaults" property instead', 'symfony/routing:7.4')] public function getDefaults(): array { return $this->defaults; } + #[\Deprecated('Use the "schemes" property instead', 'symfony/routing:7.4')] public function setSchemes(array|string $schemes): void { $this->schemes = (array) $schemes; } + #[\Deprecated('Use the "schemes" property instead', 'symfony/routing:7.4')] public function getSchemes(): array { return $this->schemes; } + #[\Deprecated('Use the "methods" property instead', 'symfony/routing:7.4')] public function setMethods(array|string $methods): void { $this->methods = (array) $methods; } + #[\Deprecated('Use the "methods" property instead', 'symfony/routing:7.4')] public function getMethods(): array { return $this->methods; } + #[\Deprecated('Use the "condition" property instead', 'symfony/routing:7.4')] public function setCondition(?string $condition): void { $this->condition = $condition; } + #[\Deprecated('Use the "condition" property instead', 'symfony/routing:7.4')] public function getCondition(): ?string { return $this->condition; } + #[\Deprecated('Use the "priority" property instead', 'symfony/routing:7.4')] public function setPriority(int $priority): void { $this->priority = $priority; } + #[\Deprecated('Use the "priority" property instead', 'symfony/routing:7.4')] public function getPriority(): ?int { return $this->priority; } + #[\Deprecated('Use the "envs" property instead', 'symfony/routing:7.4')] public function setEnv(?string $env): void { - $this->env = $env; + $this->envs = (array) $env; } + #[\Deprecated('Use the "envs" property instead', 'symfony/routing:7.4')] public function getEnv(): ?string { - return $this->env; + if (!$this->envs) { + return null; + } + if (\count($this->envs) > 1) { + throw new LogicException(\sprintf('The "env" property has %d environments. Use "getEnvs()" to get all of them.', \count($this->envs))); + } + + return $this->envs[0]; } /** * @return (string|DeprecatedAlias)[] */ + #[\Deprecated('Use the "aliases" property instead', 'symfony/routing:7.4')] public function getAliases(): array { return $this->aliases; @@ -220,6 +254,7 @@ public function getAliases(): array /** * @param string|DeprecatedAlias|(string|DeprecatedAlias)[] $aliases */ + #[\Deprecated('Use the "aliases" property instead', 'symfony/routing:7.4')] public function setAliases(string|DeprecatedAlias|array $aliases): void { $this->aliases = \is_array($aliases) ? $aliases : [$aliases]; diff --git a/CHANGELOG.md b/CHANGELOG.md index d21e550f..08126cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ CHANGELOG ========= +7.4 +--- + + * Add `AttributeServicesLoader` and `RoutingControllerPass` to auto-register routes from attributes on services + * Allow query-specific parameters in `UrlGenerator` using `_query` + * Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute + * Add argument `$parameters` to `RequestContext`'s constructor + * Handle declaring routes using PHP arrays that follow the same shape as corresponding yaml files + * Add `RoutesReference` to help writing PHP configs using yaml-like array-shapes + * Deprecate class aliases in the `Annotation` namespace, use attributes instead + * Deprecate getters and setters in attribute classes in favor of public properties + * Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead + * Deprecate XML configuration format, use YAML, PHP or attributes instead + 7.3 --- diff --git a/DependencyInjection/RoutingControllerPass.php b/DependencyInjection/RoutingControllerPass.php new file mode 100644 index 00000000..4074a6e0 --- /dev/null +++ b/DependencyInjection/RoutingControllerPass.php @@ -0,0 +1,40 @@ + + * + * 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; + +/** + * @author Nicolas Grekas + */ +final class RoutingControllerPass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('routing.loader.attribute.services')) { + return; + } + + $resolve = $container->getParameterBag()->resolveValue(...); + $taggedClasses = []; + foreach ($this->findAndSortTaggedServices('routing.controller', $container) as $id) { + $taggedClasses[$resolve($container->getDefinition($id)->getClass())] = true; + } + + $container->getDefinition('routing.loader.attribute.services') + ->replaceArgument(0, array_keys($taggedClasses)); + } +} diff --git a/Generator/Dumper/CompiledUrlGeneratorDumper.php b/Generator/Dumper/CompiledUrlGeneratorDumper.php index 555c5bfb..881a6d50 100644 --- a/Generator/Dumper/CompiledUrlGeneratorDumper.php +++ b/Generator/Dumper/CompiledUrlGeneratorDumper.php @@ -91,14 +91,14 @@ public function getCompiledAliases(): array public function dump(array $options = []): string { return <<generateDeclaredRoutes()} -]; + return [{$this->generateDeclaredRoutes()} + ]; -EOF; + EOF; } /** diff --git a/Generator/UrlGenerator.php b/Generator/UrlGenerator.php index 216b0d54..4c5ae1c9 100644 --- a/Generator/UrlGenerator.php +++ b/Generator/UrlGenerator.php @@ -142,6 +142,18 @@ public function generate(string $name, array $parameters = [], int $referenceTyp */ protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parameters, string $name, int $referenceType, array $hostTokens, array $requiredSchemes = []): string { + $queryParameters = []; + + if (isset($parameters['_query'])) { + if (\is_array($parameters['_query'])) { + $queryParameters = $parameters['_query']; + unset($parameters['_query']); + } else { + trigger_deprecation('symfony/routing', '7.4', 'Parameter "_query" is reserved for passing an array of query parameters. Passing a scalar value is deprecated and will throw an exception in Symfony 8.0.'); + // throw new InvalidParameterException('Parameter "_query" must be an array of query parameters.'); + } + } + $variables = array_flip($variables); $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); @@ -260,6 +272,7 @@ protected function doGenerate(array $variables, array $defaults, array $requirem // add a query string if needed $extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, fn ($a, $b) => $a == $b ? 0 : 1); + $extra = array_replace($extra, $queryParameters); array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) { if (\is_object($v)) { diff --git a/Loader/AttributeClassLoader.php b/Loader/AttributeClassLoader.php index f2bc668d..9739ff51 100644 --- a/Loader/AttributeClassLoader.php +++ b/Loader/AttributeClassLoader.php @@ -105,14 +105,14 @@ public function load(mixed $class, ?string $type = null): RouteCollection $globals = $this->getGlobals($class); $collection = new RouteCollection(); $collection->addResource(new ReflectionClassResource($class)); - if ($globals['env'] && $this->env !== $globals['env']) { + if ($globals['env'] && !\in_array($this->env, $globals['env'], true)) { return $collection; } $fqcnAlias = false; if (!$class->hasMethod('__invoke')) { foreach ($this->getAttributes($class) as $attr) { - if ($attr->getAliases()) { + if ($attr->aliases) { throw new InvalidArgumentException(\sprintf('Route aliases cannot be used on non-invokable class "%s".', $class->getName())); } } @@ -161,14 +161,14 @@ public function load(mixed $class, ?string $type = null): RouteCollection */ protected function addRoute(RouteCollection $collection, object $attr, array $globals, \ReflectionClass $class, \ReflectionMethod $method): void { - if ($attr->getEnv() && $attr->getEnv() !== $this->env) { + if ($attr->envs && !\in_array($this->env, $attr->envs, true)) { return; } - $name = $attr->getName() ?? $this->getDefaultRouteName($class, $method); + $name = $attr->name ?? $this->getDefaultRouteName($class, $method); $name = $globals['name'].$name; - $requirements = $attr->getRequirements(); + $requirements = $attr->requirements; foreach ($requirements as $placeholder => $requirement) { if (\is_int($placeholder)) { @@ -176,17 +176,17 @@ protected function addRoute(RouteCollection $collection, object $attr, array $gl } } - $defaults = array_replace($globals['defaults'], $attr->getDefaults()); + $defaults = array_replace($globals['defaults'], $attr->defaults); $requirements = array_replace($globals['requirements'], $requirements); - $options = array_replace($globals['options'], $attr->getOptions()); - $schemes = array_unique(array_merge($globals['schemes'], $attr->getSchemes())); - $methods = array_unique(array_merge($globals['methods'], $attr->getMethods())); + $options = array_replace($globals['options'], $attr->options); + $schemes = array_unique(array_merge($globals['schemes'], $attr->schemes)); + $methods = array_unique(array_merge($globals['methods'], $attr->methods)); - $host = $attr->getHost() ?? $globals['host']; - $condition = $attr->getCondition() ?? $globals['condition']; - $priority = $attr->getPriority() ?? $globals['priority']; + $host = $attr->host ?? $globals['host']; + $condition = $attr->condition ?? $globals['condition']; + $priority = $attr->priority ?? $globals['priority']; - $path = $attr->getLocalizedPaths() ?: $attr->getPath(); + $path = $attr->path; $prefix = $globals['localized_paths'] ?: $globals['path']; $paths = []; @@ -241,13 +241,13 @@ protected function addRoute(RouteCollection $collection, object $attr, array $gl } else { $collection->add($name, $route, $priority); } - foreach ($attr->getAliases() as $aliasAttribute) { + foreach ($attr->aliases as $aliasAttribute) { if ($aliasAttribute instanceof DeprecatedAlias) { - $alias = $collection->addAlias($aliasAttribute->getAliasName(), $name); + $alias = $collection->addAlias($aliasAttribute->aliasName, $name); $alias->setDeprecated( - $aliasAttribute->getPackage(), - $aliasAttribute->getVersion(), - $aliasAttribute->getMessage() + $aliasAttribute->package, + $aliasAttribute->version, + $aliasAttribute->message ); continue; } @@ -299,46 +299,47 @@ protected function getGlobals(\ReflectionClass $class): array if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) { $attr = $attribute->newInstance(); - if (null !== $attr->getName()) { - $globals['name'] = $attr->getName(); + if (null !== $attr->name) { + $globals['name'] = $attr->name; } - if (null !== $attr->getPath()) { - $globals['path'] = $attr->getPath(); + if (\is_string($attr->path)) { + $globals['path'] = $attr->path; + $globals['localized_paths'] = []; + } else { + $globals['localized_paths'] = $attr->path ?? []; } - $globals['localized_paths'] = $attr->getLocalizedPaths(); - - if (null !== $attr->getRequirements()) { - $globals['requirements'] = $attr->getRequirements(); + if (null !== $attr->requirements) { + $globals['requirements'] = $attr->requirements; } - if (null !== $attr->getOptions()) { - $globals['options'] = $attr->getOptions(); + if (null !== $attr->options) { + $globals['options'] = $attr->options; } - if (null !== $attr->getDefaults()) { - $globals['defaults'] = $attr->getDefaults(); + if (null !== $attr->defaults) { + $globals['defaults'] = $attr->defaults; } - if (null !== $attr->getSchemes()) { - $globals['schemes'] = $attr->getSchemes(); + if (null !== $attr->schemes) { + $globals['schemes'] = $attr->schemes; } - if (null !== $attr->getMethods()) { - $globals['methods'] = $attr->getMethods(); + if (null !== $attr->methods) { + $globals['methods'] = $attr->methods; } - if (null !== $attr->getHost()) { - $globals['host'] = $attr->getHost(); + if (null !== $attr->host) { + $globals['host'] = $attr->host; } - if (null !== $attr->getCondition()) { - $globals['condition'] = $attr->getCondition(); + if (null !== $attr->condition) { + $globals['condition'] = $attr->condition; } - $globals['priority'] = $attr->getPriority() ?? 0; - $globals['env'] = $attr->getEnv(); + $globals['priority'] = $attr->priority ?? 0; + $globals['env'] = $attr->envs; foreach ($globals['requirements'] as $placeholder => $requirement) { if (\is_int($placeholder)) { diff --git a/Loader/AttributeFileLoader.php b/Loader/AttributeFileLoader.php index ae0bad7a..103ada2d 100644 --- a/Loader/AttributeFileLoader.php +++ b/Loader/AttributeFileLoader.php @@ -79,10 +79,7 @@ protected function findClass(string $file): string|false throw new \InvalidArgumentException(\sprintf('The file "%s" does not contain PHP code. Did you forget to add the " true, \T_STRING => true]; - if (\defined('T_NAME_QUALIFIED')) { - $nsTokens[\T_NAME_QUALIFIED] = true; - } + $nsTokens = [\T_NS_SEPARATOR => true, \T_STRING => true, \T_NAME_QUALIFIED => true]; for ($i = 0; isset($tokens[$i]); ++$i) { $token = $tokens[$i]; if (!isset($token[1])) { @@ -115,7 +112,7 @@ protected function findClass(string $file): string|false if (\T_DOUBLE_COLON === $tokens[$j][0] || \T_NEW === $tokens[$j][0]) { $skipClassToken = true; break; - } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) { + } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT], true)) { break; } } diff --git a/Loader/AttributeServicesLoader.php b/Loader/AttributeServicesLoader.php new file mode 100644 index 00000000..b2eea818 --- /dev/null +++ b/Loader/AttributeServicesLoader.php @@ -0,0 +1,47 @@ + + * + * 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; + +/** + * Loads routes from a list of tagged classes by delegating to the attribute class loader. + * + * @author Nicolas Grekas + */ +final class AttributeServicesLoader extends Loader +{ + /** + * @param class-string[] $taggedClasses + */ + public function __construct( + private array $taggedClasses = [], + ) { + } + + public function load(mixed $resource, ?string $type = null): RouteCollection + { + $collection = new RouteCollection(); + + foreach ($this->taggedClasses as $class) { + $collection->addCollection($this->import($class, 'attribute')); + } + + return $collection; + } + + public function supports(mixed $resource, ?string $type = null): bool + { + return 'routing.controllers' === $resource; + } +} diff --git a/Loader/Configurator/RoutesReference.php b/Loader/Configurator/RoutesReference.php new file mode 100644 index 00000000..4a48ffc9 --- /dev/null +++ b/Loader/Configurator/RoutesReference.php @@ -0,0 +1,86 @@ + + * + * 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; + +// For the phpdoc to remain compatible with the generation of per-app Routes class, +// this file should have no "use" statements: all symbols referenced by +// the phpdoc need to be in the current namespace or be root-scoped. + +/** + * This class provides array-shapes for configuring the routes of an application. + * + * Example: + * + * ```php + * // config/routes.php + * namespace Symfony\Component\Routing\Loader\Configurator; + * + * return Routes::config([ + * 'controllers' => [ + * 'resource' => 'routing.controllers', + * ], + * ]); + * ``` + * + * @psalm-type RouteConfig = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type ImportConfig = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type AliasConfig = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type RoutesConfig = array> + */ +class RoutesReference +{ + /** + * @param RoutesConfig $config + * + * @psalm-return RoutesConfig + */ + public static function config(array $config): array + { + return $config; + } +} diff --git a/Loader/PhpFileLoader.php b/Loader/PhpFileLoader.php index adf7eed3..311a2a66 100644 --- a/Loader/PhpFileLoader.php +++ b/Loader/PhpFileLoader.php @@ -12,7 +12,11 @@ namespace Symfony\Component\Routing\Loader; use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Exception\InvalidArgumentException; +use Symfony\Component\Routing\Loader\Configurator\Routes; +use Symfony\Component\Routing\Loader\Configurator\RoutesReference; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Symfony\Component\Routing\RouteCollection; @@ -35,18 +39,42 @@ public function load(mixed $file, ?string $type = null): RouteCollection $path = $this->locator->locate($file); $this->setCurrentDir(\dirname($path)); + // Expose RoutesReference::config() as Routes::config() + if (!class_exists(Routes::class)) { + class_alias(RoutesReference::class, Routes::class); + } + // 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); + }, null, null); + + try { + if (1 === $result = $load($path)) { + $result = null; + } + } catch (\Error $e) { + $load = \Closure::bind(static function ($file) use ($loader) { + return include $file; + }, null, ProtectedPhpFileLoader::class); - $result = $load($path); + if (1 === $result = $load($path)) { + $result = null; + } + + trigger_deprecation('symfony/routing', '7.4', 'Accessing the internal scope of the loader in config files is deprecated, use only its public API instead in "%s" on line %d.', $e->getFile(), $e->getLine()); + } if (\is_object($result) && \is_callable($result)) { $collection = $this->callConfigurator($result, $path, $file); - } else { - $collection = $result; + } elseif (\is_array($result)) { + $collection = new RouteCollection(); + $loader = new YamlFileLoader($this->locator, $this->env); + $loader->setResolver($this->resolver ?? new LoaderResolver([$this])); + (new \ReflectionMethod(YamlFileLoader::class, 'loadContent'))->invoke($loader, $collection, $result, $path, $file); + } elseif (!($collection = $result) instanceof RouteCollection) { + throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is expected to be a RouteCollection, an array or a configurator callable, but got "%s".', $path, get_debug_type($result))); } $collection->addResource(new FileResource($path)); @@ -59,11 +87,11 @@ 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 + protected function callConfigurator(callable $callback, string $path, string $file): RouteCollection { $collection = new RouteCollection(); - $result(new RoutingConfigurator($collection, $this, $path, $file, $this->env)); + $callback(new RoutingConfigurator($collection, $this, $path, $file, $this->env)); return $collection; } diff --git a/Loader/XmlFileLoader.php b/Loader/XmlFileLoader.php index c7275962..1ee2f813 100644 --- a/Loader/XmlFileLoader.php +++ b/Loader/XmlFileLoader.php @@ -24,6 +24,8 @@ * * @author Fabien Potencier * @author Tobias Schultze + * + * @deprecated since Symfony 7.4, use another loader instead */ class XmlFileLoader extends FileLoader { @@ -40,6 +42,8 @@ class XmlFileLoader extends FileLoader */ public function load(mixed $file, ?string $type = null): RouteCollection { + trigger_deprecation('symfony/routing', '7.4', 'XML configuration format is deprecated, use YAML, PHP or attributes instead.'); + $path = $this->locator->locate($file); $xml = $this->loadFile($path); diff --git a/Loader/YamlFileLoader.php b/Loader/YamlFileLoader.php index 3e40e8bb..63a96c99 100644 --- a/Loader/YamlFileLoader.php +++ b/Loader/YamlFileLoader.php @@ -74,33 +74,7 @@ public function load(mixed $file, ?string $type = null): RouteCollection throw new \InvalidArgumentException(\sprintf('The file "%s" must contain a YAML array.', $path)); } - foreach ($parsedConfig as $name => $config) { - if (str_starts_with($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); - } - } + $this->loadContent($collection, $parsedConfig, $path, $file); return $collection; } @@ -246,7 +220,7 @@ protected function parseImport(RouteCollection $collection, array $config, strin protected function validate(mixed $config, string $name, string $path): void { if (!\is_array($config)) { - throw new \InvalidArgumentException(\sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path)); + throw new \InvalidArgumentException(\sprintf('The definition of "%s" in "%s" must be an array.', $name, $path)); } if (isset($config['alias'])) { $this->validateAlias($config, $name, $path); @@ -273,6 +247,29 @@ protected function validate(mixed $config, string $name, string $path): void } } + private function loadContent(RouteCollection $collection, array $config, string $path, string $file): void + { + foreach ($config as $name => $config) { + if (!str_starts_with($when = $name, 'when@')) { + $config = [$name => $config]; + } elseif (!$this->env || 'when@'.$this->env !== $name) { + continue; + } else { + $when .= '" when "@'.$this->env; + } + + foreach ($config as $name => $config) { + $this->validate($config, $when, $path); + + if (isset($config['resource'])) { + $this->parseImport($collection, $config, $path, $file); + } else { + $this->parseRoute($collection, $name, $config, $path); + } + } + } + } + /** * @throws \InvalidArgumentException If one of the provided config keys is not supported, * something is missing or the combination is nonsense diff --git a/Loader/schema/routing.schema.json b/Loader/schema/routing.schema.json new file mode 100644 index 00000000..f2e2664f --- /dev/null +++ b/Loader/schema/routing.schema.json @@ -0,0 +1,161 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Symfony Routing Configuration", + "description": "Defines the application's URL routes, including imports and environment-specific conditionals.", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9_.-]+$": { + "oneOf": [ + { "$ref": "#/$defs/routeDefinition" }, + { "$ref": "#/$defs/routeImport" }, + { "$ref": "#/$defs/routeAlias" } + ] + }, + "^when@.+$": { + "$ref": "#", + "description": "A container for routes that are only loaded in a specific environment (e.g., 'when@dev')." + } + }, + "additionalProperties": false, + "$defs": { + "routeDefinition": { + "type": "object", + "properties": { + "path": { + "oneOf": [ + { "type": "string" }, + { "type": "object", "patternProperties": { "^.+$": { "type": "string" } }, "additionalProperties": false } + ], + "description": "The URL path or a map of locale=>path for localized routes." + }, + "controller": { + "type": "string", + "description": "The controller that handles the request, e.g., 'App\\Controller\\BlogController::show'." + }, + "methods": { + "description": "The HTTP method(s) this route matches.", + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "requirements": { + "type": "object", + "description": "Regular expression constraints for path parameters.", + "additionalProperties": { "type": "string" } + }, + "defaults": { "type": "object" }, + "options": { "type": "object" }, + "host": { + "oneOf": [ + { "type": "string" }, + { "type": "object", "patternProperties": { "^.+$": { "type": "string" } }, "additionalProperties": false } + ] + }, + "schemes": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "condition": { "type": "string" }, + "locale": { "type": "string" }, + "format": { "type": "string" }, + "utf8": { "type": "boolean" }, + "stateless": { "type": "boolean" }, + "deprecated": { + "type": "object", + "properties": { + "package": { "type": "string" }, + "version": { "type": "string" }, + "message": { "type": "string" } + }, + "required": ["package", "version"], + "additionalProperties": false + } + }, + "required": ["path"], + "additionalProperties": false + }, + "routeImport": { + "type": "object", + "properties": { + "resource": { + "description": "Path to the resource to import (commonly a string or {path, namespace}), array of paths, or custom value for loaders (additional properties allowed for extensions).", + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } }, + { + "type": "object", + "properties": { + "path": { "type": "string", "description": "The directory path to the resource." }, + "namespace": { "type": "string", "description": "The namespace of the controllers in the imported resource (e.g., 'App\\Availability\\UserInterface\\Api')." } + }, + "required": ["path"], + "additionalProperties": true + } + ] + }, + "type": { + "type": "string", + "description": "The type of the resource (e.g., 'attribute', 'annotation', 'yaml')." + }, + "prefix": { + "oneOf": [ + { "type": "string" }, + { "type": "object", "patternProperties": { "^.+$": { "type": "string" } }, "additionalProperties": false } + ], + "description": "A URL prefix to apply to all routes from the imported resource." + }, + "name_prefix": { + "type": "string", + "description": "A name prefix to apply to all routes from the imported resource." + }, + "requirements": { "type": "object", "additionalProperties": { "type": "string" } }, + "defaults": { "type": "object" }, + "options": { "type": "object" }, + "host": { + "oneOf": [ + { "type": "string" }, + { "type": "object", "patternProperties": { "^.+$": { "type": "string" } }, "additionalProperties": false } + ] + }, + "schemes": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "condition": { "type": "string" }, + "trailing_slash_on_root": { "type": "boolean" }, + "methods": { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" } } ] }, + "locale": { "type": "string" }, + "format": { "type": "string" }, + "utf8": { "type": "boolean" }, + "exclude": { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" } } ] }, + "stateless": { "type": "boolean" }, + "controller": { "type": "string" } + }, + "required": ["resource"], + "additionalProperties": false + }, + "routeAlias": { + "type": "object", + "properties": { + "alias": { "type": "string" }, + "deprecated": { + "type": "object", + "properties": { + "package": { "type": "string" }, + "version": { "type": "string" }, + "message": { "type": "string" } + }, + "required": ["package", "version"], + "additionalProperties": false + } + }, + "required": ["alias"], + "additionalProperties": false + } + } +} diff --git a/Matcher/Dumper/CompiledUrlMatcherDumper.php b/Matcher/Dumper/CompiledUrlMatcherDumper.php index d55b2d5f..4060e73b 100644 --- a/Matcher/Dumper/CompiledUrlMatcherDumper.php +++ b/Matcher/Dumper/CompiledUrlMatcherDumper.php @@ -37,17 +37,17 @@ class CompiledUrlMatcherDumper extends MatcherDumper public function dump(array $options = []): string { return <<generateCompiledRoutes()}]; + return [ + {$this->generateCompiledRoutes()}]; -EOF; + EOF; } public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider): void @@ -112,12 +112,12 @@ public function getCompiledRoutes(bool $forDump = false): array } $checkConditionCode = <<indent(implode("\n", $conditions), 3)} - } - } -EOF; + static function (\$condition, \$context, \$request, \$params) { // \$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; @@ -470,11 +470,10 @@ public static function export(mixed $value): string if (null === $value) { return 'null'; } + if (\is_object($value)) { + throw new \InvalidArgumentException(\sprintf('Symfony\Component\Routing\Route cannot contain objects, but "%s" given.', get_debug_type($value))); + } 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) { diff --git a/README.md b/README.md index 75580363..4f8634a7 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,6 @@ $url = $generator->generate('blog_show', [ Sponsor ------- -The Routing component for Symfony 7.1 is [backed][1] by [redirection.io][2]. - -redirection.io logs all your website’s HTTP traffic, and lets you fix errors -with redirect rules in seconds. Give your marketing, SEO and IT teams the -right tool to manage your website traffic efficiently! - Help Symfony by [sponsoring][3] its development! Resources @@ -61,6 +55,4 @@ Resources [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) -[1]: https://symfony.com/backers -[2]: https://redirection.io [3]: https://symfony.com/sponsor diff --git a/RequestContext.php b/RequestContext.php index 5e9e79d9..8c560370 100644 --- a/RequestContext.php +++ b/RequestContext.php @@ -33,7 +33,7 @@ class RequestContext 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 = '') + public function __construct(string $baseUrl = '', string $method = 'GET', string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443, string $path = '/', string $queryString = '', ?array $parameters = null) { $this->setBaseUrl($baseUrl); $this->setMethod($method); @@ -43,6 +43,7 @@ public function __construct(string $baseUrl = '', string $method = 'GET', string $this->setHttpsPort($httpsPort); $this->setPathInfo($path); $this->setQueryString($queryString); + $this->parameters = $parameters ?? $this->parameters; } public static function fromUri(string $uri, string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443): self diff --git a/Tests/Attribute/RouteTest.php b/Tests/Attribute/RouteTest.php index bbaa7563..1ca2b98b 100644 --- a/Tests/Attribute/RouteTest.php +++ b/Tests/Attribute/RouteTest.php @@ -11,36 +11,35 @@ namespace Symfony\Component\Routing\Tests\Attribute; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\FooController; class RouteTest extends TestCase { - /** - * @dataProvider getValidParameters - */ - public function testLoadFromAttribute(string $methodName, string $getter, mixed $expectedReturn) + #[DataProvider('getValidParameters')] + public function testLoadFromAttribute(string $methodName, string $property, mixed $expectedReturn) { $route = (new \ReflectionMethod(FooController::class, $methodName))->getAttributes(Route::class)[0]->newInstance(); - $this->assertEquals($route->$getter(), $expectedReturn); + $this->assertEquals($route->$property, $expectedReturn); } public static function getValidParameters(): iterable { return [ - ['simplePath', 'getPath', '/Blog'], - ['localized', 'getLocalizedPaths', ['nl' => '/hier', 'en' => '/here']], - ['requirements', 'getRequirements', ['locale' => 'en']], - ['options', 'getOptions', ['compiler_class' => 'RouteCompiler']], - ['name', 'getName', 'blog_index'], - ['defaults', 'getDefaults', ['_controller' => 'MyBlogBundle:Blog:index']], - ['schemes', 'getSchemes', ['https']], - ['methods', 'getMethods', ['GET', 'POST']], - ['host', 'getHost', '{locale}.example.com'], - ['condition', 'getCondition', 'context.getMethod() == \'GET\''], - ['alias', 'getAliases', ['alias', 'completely_different_name']], + ['simplePath', 'path', '/Blog'], + ['localized', 'path', ['nl' => '/hier', 'en' => '/here']], + ['requirements', 'requirements', ['locale' => 'en']], + ['options', 'options', ['compiler_class' => 'RouteCompiler']], + ['name', 'name', 'blog_index'], + ['defaults', 'defaults', ['_controller' => 'MyBlogBundle:Blog:index']], + ['schemes', 'schemes', ['https']], + ['methods', 'methods', ['GET', 'POST']], + ['host', 'host', '{locale}.example.com'], + ['condition', 'condition', 'context.getMethod() == \'GET\''], + ['alias', 'aliases', ['alias', 'completely_different_name']], ]; } } diff --git a/Tests/DependencyInjection/RoutingControllerPassTest.php b/Tests/DependencyInjection/RoutingControllerPassTest.php new file mode 100644 index 00000000..d743a338 --- /dev/null +++ b/Tests/DependencyInjection/RoutingControllerPassTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\Routing\DependencyInjection\RoutingControllerPass; + +class RoutingControllerPassTest extends TestCase +{ + public function testProcessInjectsTaggedControllerClassesOrderedAndUnique() + { + $container = new ContainerBuilder(); + $container->setParameter('ctrl_a.class', CtrlA::class); + + $container->register('routing.loader.attribute.services', \stdClass::class) + ->setArguments([null]); + + $container->register('ctrl_a', '%ctrl_a.class%')->addTag('routing.controller', ['priority' => 10]); + $container->register('ctrl_b', CtrlB::class)->addTag('routing.controller', ['priority' => 20]); + $container->register('ctrl_c', CtrlC::class)->addTag('routing.controller', ['priority' => -5]); + + (new RoutingControllerPass())->process($container); + + $this->assertSame([ + CtrlB::class, + CtrlA::class, + CtrlC::class, + ], $container->getDefinition('routing.loader.attribute.services')->getArgument(0)); + } + + public function testProcessWithNoTaggedControllersSetsEmptyList() + { + $container = new ContainerBuilder(); + + $loaderDef = new Definition(\stdClass::class); + $loaderDef->setArguments([['preexisting']]); + $container->setDefinition('routing.loader.attribute.services', $loaderDef); + + (new RoutingControllerPass())->process($container); + + $this->assertSame([], $container->getDefinition('routing.loader.attribute.services')->getArgument(0)); + } +} diff --git a/Tests/Fixtures/AttributeFixtures/RouteWithEnv.php b/Tests/Fixtures/AttributeFixtures/RouteWithEnv.php index 31f6c39b..c8f82fdf 100644 --- a/Tests/Fixtures/AttributeFixtures/RouteWithEnv.php +++ b/Tests/Fixtures/AttributeFixtures/RouteWithEnv.php @@ -16,4 +16,19 @@ public function action() public function action2() { } + + #[Route(path: '/path3', name: 'action3', env: ['some-other-env', 'some-other-env-two'])] + public function action3() + { + } + + #[Route(path: '/path4', name: 'action4', env: null)] + public function action4() + { + } + + #[Route(path: '/path5', name: 'action5', env: ['some-other-env', 'some-env'])] + public function action5() + { + } } diff --git a/Tests/Fixtures/array_routes.php b/Tests/Fixtures/array_routes.php new file mode 100644 index 00000000..3a4af646 --- /dev/null +++ b/Tests/Fixtures/array_routes.php @@ -0,0 +1,8 @@ + ['path' => '/a'], + 'b' => ['path' => '/b', 'methods' => ['GET']], +]); diff --git a/Tests/Fixtures/array_when_env.php b/Tests/Fixtures/array_when_env.php new file mode 100644 index 00000000..e0f20ab5 --- /dev/null +++ b/Tests/Fixtures/array_when_env.php @@ -0,0 +1,10 @@ + [ + 'x' => ['path' => '/x'], + ], + 'a' => ['path' => '/a'], +]); diff --git a/Tests/Fixtures/dumper/compiled_url_matcher13.php b/Tests/Fixtures/dumper/compiled_url_matcher13.php index 63252943..466550c3 100644 --- a/Tests/Fixtures/dumper/compiled_url_matcher13.php +++ b/Tests/Fixtures/dumper/compiled_url_matcher13.php @@ -11,15 +11,15 @@ ], [ // $regexpList 0 => '{^(?' - .'|(?i:([^\\.]++)\\.exampple\\.com)\\.(?' + .'|(?i:([^\\.]++)\\.example\\.com)\\.(?' .'|/abc([^/]++)(?' - .'|(*:56)' + .'|(*:55)' .')' .')' .')/?$}sD', ], [ // $dynamicRoutes - 56 => [ + 55 => [ [['_route' => 'r1'], ['foo', 'foo'], null, null, false, true, null], [['_route' => 'r2'], ['foo', 'foo'], null, null, false, true, null], [null, null, null, null, false, false, 0], diff --git a/Tests/Fixtures/importer-php-returns-array-with-import.yml b/Tests/Fixtures/importer-php-returns-array-with-import.yml new file mode 100644 index 00000000..21fbbbbd --- /dev/null +++ b/Tests/Fixtures/importer-php-returns-array-with-import.yml @@ -0,0 +1,3 @@ +routes_from_php: + resource: validpattern.php + type: php diff --git a/Tests/Fixtures/importer-php-returns-array.php b/Tests/Fixtures/importer-php-returns-array.php new file mode 100644 index 00000000..2ca32369 --- /dev/null +++ b/Tests/Fixtures/importer-php-returns-array.php @@ -0,0 +1,10 @@ + [ + 'resource' => 'importer-php-returns-array-with-import.yml', + ], + 'direct' => [ + 'path' => '/direct', + ], +]; diff --git a/Tests/Fixtures/legacy_internal_scope.php b/Tests/Fixtures/legacy_internal_scope.php new file mode 100644 index 00000000..5de3a9e7 --- /dev/null +++ b/Tests/Fixtures/legacy_internal_scope.php @@ -0,0 +1,8 @@ +callConfigurator(static fn () => [], 'dummy.php', 'dummy.php'); + +return new RouteCollection(); diff --git a/Tests/Fixtures/when-env.php b/Tests/Fixtures/when-env.php new file mode 100644 index 00000000..cc60e4c8 --- /dev/null +++ b/Tests/Fixtures/when-env.php @@ -0,0 +1,15 @@ +env()) { + $routes->add('b', '/b'); + $routes->add('a', '/a2'); + } elseif ('some-other-env' === $routes->env()) { + $routes->add('a', '/a3'); + $routes->add('c', '/c'); + } + + $routes->add('a', '/a1'); +}; diff --git a/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php b/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php index 8edc49a6..c461cad1 100644 --- a/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php +++ b/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Routing\Tests\Generator\Dumper; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Routing\Exception\RouteCircularReferenceException; use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\Generator\CompiledUrlGenerator; @@ -24,8 +24,6 @@ class CompiledUrlGeneratorDumperTest extends TestCase { - use ExpectUserDeprecationMessageTrait; - private RouteCollection $routeCollection; private CompiledUrlGeneratorDumper $generatorDumper; private string $testTmpFilepath; @@ -338,9 +336,7 @@ public function testIndirectCircularReferenceShouldThrowAnException() $this->generatorDumper->dump(); } - /** - * @group legacy - */ + #[IgnoreDeprecations] public function testDeprecatedAlias() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: The "b" route alias is deprecated. You should stop using it, as it will be removed in the future.'); @@ -356,9 +352,7 @@ public function testDeprecatedAlias() $compiledUrlGenerator->generate('b'); } - /** - * @group legacy - */ + #[IgnoreDeprecations] public function testDeprecatedAliasWithCustomMessage() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); @@ -374,9 +368,7 @@ public function testDeprecatedAliasWithCustomMessage() $compiledUrlGenerator->generate('b'); } - /** - * @group legacy - */ + #[IgnoreDeprecations] public function testTargettingADeprecatedAliasShouldTriggerDeprecation() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); diff --git a/Tests/Generator/UrlGeneratorTest.php b/Tests/Generator/UrlGeneratorTest.php index 25a4c674..9abbef7d 100644 --- a/Tests/Generator/UrlGeneratorTest.php +++ b/Tests/Generator/UrlGeneratorTest.php @@ -11,9 +11,11 @@ namespace Symfony\Component\Routing\Tests\Generator; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; -use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; use Symfony\Component\Routing\Exception\RouteCircularReferenceException; @@ -26,8 +28,6 @@ class UrlGeneratorTest extends TestCase { - use ExpectUserDeprecationMessageTrait; - public function testAbsoluteUrlWithPort80() { $routes = $this->getRoutes('test', new Route('/testing')); @@ -110,9 +110,7 @@ public function testNotPassedOptionalParameterInBetween() $this->assertSame('/app.php/', $this->getGenerator($routes)->generate('test')); } - /** - * @dataProvider valuesProvider - */ + #[DataProvider('valuesProvider')] public function testRelativeUrlWithExtraParameters(string $expectedQueryString, string $parameter, $value) { $routes = $this->getRoutes('test', new Route('/testing')); @@ -121,9 +119,7 @@ public function testRelativeUrlWithExtraParameters(string $expectedQueryString, $this->assertSame('/app.php/testing'.$expectedQueryString, $url); } - /** - * @dataProvider valuesProvider - */ + #[DataProvider('valuesProvider')] public function testAbsoluteUrlWithExtraParameters(string $expectedQueryString, string $parameter, $value) { $routes = $this->getRoutes('test', new Route('/testing')); @@ -153,6 +149,7 @@ public static function valuesProvider(): array 'stdClass in nested stdClass' => ['?foo%5Bnested%5D%5Bbaz%5D=bar', 'foo', $nestedStdClass], 'non stringable object' => ['', 'foo', new NonStringableObject()], 'non stringable object but has public property' => ['?foo%5Bfoo%5D=property', 'foo', new NonStringableObjectWithPublicProperty()], + 'numeric key' => ['?123=foo', '123', 'foo'], ]; } @@ -806,9 +803,7 @@ public function testAliasWhichTargetRouteDoesntExist() $this->getGenerator($routes)->generate('d'); } - /** - * @group legacy - */ + #[IgnoreDeprecations] public function testDeprecatedAlias() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: The "b" route alias is deprecated. You should stop using it, as it will be removed in the future.'); @@ -821,9 +816,7 @@ public function testDeprecatedAlias() $this->getGenerator($routes)->generate('b'); } - /** - * @group legacy - */ + #[IgnoreDeprecations] public function testDeprecatedAliasWithCustomMessage() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); @@ -836,9 +829,7 @@ public function testDeprecatedAliasWithCustomMessage() $this->getGenerator($routes)->generate('b'); } - /** - * @group legacy - */ + #[IgnoreDeprecations] public function testTargettingADeprecatedAliasShouldTriggerDeprecation() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); @@ -890,9 +881,7 @@ public function testIndirectCircularReferenceShouldThrowAnException() $this->getGenerator($routes)->generate('a'); } - /** - * @dataProvider provideRelativePaths - */ + #[DataProvider('provideRelativePaths')] public function testGetRelativePath($sourcePath, $targetPath, $expectedPath) { $this->assertSame($expectedPath, UrlGenerator::getRelativePath($sourcePath, $targetPath)); @@ -1031,9 +1020,7 @@ public function testFragmentsCanBeDefinedAsDefaults() $this->assertEquals('/app.php/testing#fragment', $url); } - /** - * @dataProvider provideLookAroundRequirementsInPath - */ + #[DataProvider('provideLookAroundRequirementsInPath')] public function testLookRoundRequirementsInPath($expected, $path, $requirement) { $routes = $this->getRoutes('test', new Route($path, [], ['foo' => $requirement, 'baz' => '.+?'])); @@ -1054,6 +1041,79 @@ public function testUtf8VarName() $this->assertSame('/app.php/foo/baz', $this->getGenerator($routes)->generate('test', ['bär' => 'baz'])); } + public function testQueryParameters() + { + $routes = $this->getRoutes('user', new Route('/user/{username}')); + $url = $this->getGenerator($routes)->generate('user', [ + 'username' => 'john', + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'baz', + '_query' => [ + 'a' => '123', + 'd' => '789', + ], + ]); + $this->assertSame('/app.php/user/john?a=123&b=bar&c=baz&d=789', $url); + } + + public function testRouteHostParameterAndQueryParameterWithSameName() + { + $routes = $this->getRoutes('admin_stats', new Route('/admin/stats', requirements: ['domain' => '.+'], host: '{siteCode}.{domain}')); + $url = $this->getGenerator($routes)->generate('admin_stats', [ + 'siteCode' => 'fr', + 'domain' => 'example.com', + '_query' => [ + 'siteCode' => 'us', + ], + ], UrlGeneratorInterface::NETWORK_PATH); + $this->assertSame('//fr.example.com/app.php/admin/stats?siteCode=us', $url); + } + + public function testRoutePathParameterAndQueryParameterWithSameName() + { + $routes = $this->getRoutes('user', new Route('/user/{id}')); + $url = $this->getGenerator($routes)->generate('user', [ + 'id' => '123', + '_query' => [ + 'id' => '456', + ], + ]); + $this->assertSame('/app.php/user/123?id=456', $url); + } + + public function testQueryParameterCannotSubstituteRouteParameter() + { + $routes = $this->getRoutes('user', new Route('/user/{id}')); + + $this->expectException(MissingMandatoryParametersException::class); + $this->expectExceptionMessage('Some mandatory parameters are missing ("id") to generate a URL for route "user".'); + + $this->getGenerator($routes)->generate('user', [ + '_query' => [ + 'id' => '456', + ], + ]); + } + + #[IgnoreDeprecations] + #[Group('legacy')] + public function testQueryParametersWithScalarValue() + { + $routes = $this->getRoutes('user', new Route('/user/{id}')); + + $this->expectUserDeprecationMessage( + 'Since symfony/routing 7.4: Parameter "_query" is reserved for passing an array of query parameters. '. + 'Passing a scalar value is deprecated and will throw an exception in Symfony 8.0.', + ); + + $url = $this->getGenerator($routes)->generate('user', [ + 'id' => '123', + '_query' => 'foo', + ]); + $this->assertSame('/app.php/user/123?_query=foo', $url); + } + protected function getGenerator(RouteCollection $routes, array $parameters = [], $logger = null, ?string $defaultLocale = null) { $context = new RequestContext('/app.php'); diff --git a/Tests/Loader/AttributeClassLoaderTest.php b/Tests/Loader/AttributeClassLoaderTest.php index 022e0c9f..0936b35b 100644 --- a/Tests/Loader/AttributeClassLoaderTest.php +++ b/Tests/Loader/AttributeClassLoaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Alias; use Symfony\Component\Routing\Exception\LogicException; @@ -67,9 +68,7 @@ public function testGetResolver() $loader->getResolver(); } - /** - * @dataProvider provideTestSupportsChecksResource - */ + #[DataProvider('provideTestSupportsChecksResource')] public function testSupportsChecksResource($resource, $expectedSupports) { $this->assertSame($expectedSupports, $this->loader->supports($resource), '->supports() returns true if the resource is loadable'); @@ -335,8 +334,10 @@ public function testWhenEnv() $this->setUp('some-env'); $routes = $this->loader->load(RouteWithEnv::class); - $this->assertCount(1, $routes); + $this->assertCount(3, $routes); $this->assertSame('/path', $routes->get('action')->getPath()); + $this->assertSame('/path4', $routes->get('action4')->getPath()); + $this->assertSame('/path5', $routes->get('action5')->getPath()); } public function testMethodsAndSchemes() diff --git a/Tests/Loader/AttributeServicesLoaderTest.php b/Tests/Loader/AttributeServicesLoaderTest.php new file mode 100644 index 00000000..90591b0d --- /dev/null +++ b/Tests/Loader/AttributeServicesLoaderTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Loader; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Loader\LoaderResolver; +use Symfony\Component\Routing\Loader\AttributeServicesLoader; +use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\ActionPathController; +use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\MethodActionControllers; +use Symfony\Component\Routing\Tests\Fixtures\TraceableAttributeClassLoader; + +class AttributeServicesLoaderTest extends TestCase +{ + public function testSupports() + { + $loader = new AttributeServicesLoader(); + + $this->assertFalse($loader->supports('attributes', null)); + $this->assertFalse($loader->supports('attributes', 'attribute')); + $this->assertFalse($loader->supports('other', 'routing.controllers')); + $this->assertTrue($loader->supports('routing.controllers')); + } + + public function testDelegatesToAttributeLoaderAndMergesCollections() + { + $attributeLoader = new TraceableAttributeClassLoader(); + + $servicesLoader = new AttributeServicesLoader([ + ActionPathController::class, + MethodActionControllers::class, + ]); + + $resolver = new LoaderResolver([ + $attributeLoader, + $servicesLoader, + ]); + + $attributeLoader->setResolver($resolver); + $servicesLoader->setResolver($resolver); + + $collection = $servicesLoader->load('routing.controllers'); + + $this->assertArrayHasKey('action', $collection->all()); + $this->assertArrayHasKey('put', $collection->all()); + $this->assertArrayHasKey('post', $collection->all()); + + $this->assertSame(['/path'], [$collection->get('action')->getPath()]); + $this->assertSame('/the/path', $collection->get('put')->getPath()); + $this->assertSame('/the/path', $collection->get('post')->getPath()); + + $this->assertSame([ + ActionPathController::class, + MethodActionControllers::class, + ], $attributeLoader->foundClasses); + } +} diff --git a/Tests/Loader/ContainerLoaderTest.php b/Tests/Loader/ContainerLoaderTest.php index e4f99238..967a17b1 100644 --- a/Tests/Loader/ContainerLoaderTest.php +++ b/Tests/Loader/ContainerLoaderTest.php @@ -11,15 +11,14 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Routing\Loader\ContainerLoader; class ContainerLoaderTest extends TestCase { - /** - * @dataProvider supportsProvider - */ + #[DataProvider('supportsProvider')] public function testSupports(bool $expected, ?string $type = null) { $this->assertSame($expected, (new ContainerLoader(new Container()))->supports('foo', $type)); diff --git a/Tests/Loader/DirectoryLoaderTest.php b/Tests/Loader/DirectoryLoaderTest.php index 4315588f..706238f2 100644 --- a/Tests/Loader/DirectoryLoaderTest.php +++ b/Tests/Loader/DirectoryLoaderTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Routing\Loader\AttributeFileLoader; use Symfony\Component\Routing\Loader\DirectoryLoader; use Symfony\Component\Routing\Loader\YamlFileLoader; +use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Tests\Fixtures\TraceableAttributeClassLoader; @@ -53,7 +54,7 @@ private function verifyCollection(RouteCollection $collection) $routes = $collection->all(); $this->assertCount(3, $routes, 'Three routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); for ($i = 1; $i <= 3; ++$i) { $this->assertSame('/route/'.$i, $routes['route'.$i]->getPath()); diff --git a/Tests/Loader/ObjectLoaderTest.php b/Tests/Loader/ObjectLoaderTest.php index 42743fed..1b0d2673 100644 --- a/Tests/Loader/ObjectLoaderTest.php +++ b/Tests/Loader/ObjectLoaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Loader\ObjectLoader; use Symfony\Component\Routing\Route; @@ -40,9 +41,7 @@ public function testLoadCallsServiceAndReturnsCollection() $this->assertNotEmpty($actualRoutes->getResources()); } - /** - * @dataProvider getBadResourceStrings - */ + #[DataProvider('getBadResourceStrings')] public function testExceptionWithoutSyntax(string $resourceString) { $loader = new TestObjectLoader(); diff --git a/Tests/Loader/PhpFileLoaderTest.php b/Tests/Loader/PhpFileLoaderTest.php index 16071e5b..b28a656d 100644 --- a/Tests/Loader/PhpFileLoaderTest.php +++ b/Tests/Loader/PhpFileLoaderTest.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\Routing\Loader\AttributeClassLoader; use Symfony\Component\Routing\Loader\PhpFileLoader; use Symfony\Component\Routing\Loader\Psr4DirectoryLoader; @@ -42,7 +46,7 @@ public function testLoadWithRoute() $routes = $routeCollection->all(); $this->assertCount(1, $routes, 'One route is loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); foreach ($routes as $route) { $this->assertSame('/blog/{slug}', $route->getPath()); @@ -62,7 +66,7 @@ public function testLoadWithImport() $routes = $routeCollection->all(); $this->assertCount(1, $routes, 'One route is loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); foreach ($routes as $route) { $this->assertSame('/prefix/blog/{slug}', $route->getPath()); @@ -81,7 +85,7 @@ public function testThatDefiningVariableInConfigFileHasNoSideEffects() $routeCollection = $loader->load('with_define_path_variable.php'); $resources = $routeCollection->getResources(); $this->assertCount(1, $resources); - $this->assertContainsOnly('Symfony\Component\Config\Resource\ResourceInterface', $resources); + $this->assertContainsOnlyInstancesOf(ResourceInterface::class, $resources); $fileResource = reset($resources); $this->assertSame( realpath($locator->locate('with_define_path_variable.php')), @@ -256,6 +260,20 @@ public function testRoutingConfiguratorCanImportGlobPatterns() $this->assertSame('AppBundle:Baz:view', $route->getDefault('_controller')); } + #[IgnoreDeprecations] + #[Group('legacy')] + public function testTriggersDeprecationWhenAccessingLoaderInternalScope() + { + $locator = new FileLocator([__DIR__.'/../Fixtures']); + $loader = new PhpFileLoader($locator); + + $this->expectUserDeprecationMessageMatches('{^Since symfony/routing 7.4: Accessing the internal scope of the loader in config files is deprecated, use only its public API instead in ".+" on line \d+\.$}'); + + $routes = $loader->load('legacy_internal_scope.php'); + + $this->assertInstanceOf(RouteCollection::class, $routes); + } + public function testRoutingI18nConfigurator() { $locator = new FileLocator([__DIR__.'/../Fixtures']); @@ -337,9 +355,46 @@ public function testImportingAliases() $this->assertEquals($expectedRoutes('php'), $routes); } - /** - * @dataProvider providePsr4ConfigFiles - */ + public function testWhenEnv() + { + $locator = new FileLocator([__DIR__.'/../Fixtures']); + $loader = new PhpFileLoader($locator, 'some-env'); + $routes = $loader->load('when-env.php'); + + $this->assertSame(['b', 'a'], array_keys($routes->all())); + $this->assertSame('/b', $routes->get('b')->getPath()); + } + + public function testLoadsArrayRoutes() + { + $loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); + $routes = $loader->load('array_routes.php'); + $this->assertSame('/a', $routes->get('a')->getPath()); + $this->assertSame('/b', $routes->get('b')->getPath()); + $this->assertSame(['GET'], $routes->get('b')->getMethods()); + } + + public function testWhenEnvWithArray() + { + $locator = new FileLocator([__DIR__.'/../Fixtures']); + $loader = new PhpFileLoader($locator, 'some-env'); + $routes = $loader->load('array_when_env.php'); + $this->assertSame('/a', $routes->get('a')->getPath()); + $this->assertSame('/x', $routes->get('x')->getPath()); + } + + public function testYamlImportsAreResolvedWhenProcessingPhpReturnedArrays() + { + $locator = new FileLocator([__DIR__.'/../Fixtures']); + $loader = new PhpFileLoader($locator); + + $routes = $loader->load('importer-php-returns-array.php'); + + $this->assertSame('/blog/{slug}', $routes->get('blog_show')->getPath()); + $this->assertSame('/direct', $routes->get('direct')->getPath()); + } + + #[DataProvider('providePsr4ConfigFiles')] public function testImportAttributesWithPsr4Prefix(string $configFile) { $locator = new FileLocator(\dirname(__DIR__).'/Fixtures'); diff --git a/Tests/Loader/Psr4DirectoryLoaderTest.php b/Tests/Loader/Psr4DirectoryLoaderTest.php index 0720caca..9039ef9f 100644 --- a/Tests/Loader/Psr4DirectoryLoaderTest.php +++ b/Tests/Loader/Psr4DirectoryLoaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\DelegatingLoader; @@ -66,9 +67,7 @@ public function testAbstractController() $this->assertSame(MyChildController::class.'::someAction', $route->getDefault('_controller')); } - /** - * @dataProvider provideNamespacesThatNeedTrimming - */ + #[DataProvider('provideNamespacesThatNeedTrimming')] public function testPsr4NamespaceTrim(string $namespace) { $route = $this->getLoader() @@ -91,9 +90,7 @@ public static function provideNamespacesThatNeedTrimming(): array ]; } - /** - * @dataProvider provideInvalidPsr4Namespaces - */ + #[DataProvider('provideInvalidPsr4Namespaces')] public function testInvalidPsr4Namespace(string $namespace, string $expectedExceptionMessage) { $this->expectException(InvalidArgumentException::class); diff --git a/Tests/Loader/XmlFileLoaderTest.php b/Tests/Loader/XmlFileLoaderTest.php index 7afc3d2e..5c41009f 100644 --- a/Tests/Loader/XmlFileLoaderTest.php +++ b/Tests/Loader/XmlFileLoaderTest.php @@ -11,6 +11,9 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderResolver; @@ -23,6 +26,8 @@ use Symfony\Component\Routing\Tests\Fixtures\CustomXmlFileLoader; use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\MyController; +#[IgnoreDeprecations] +#[Group('legacy')] class XmlFileLoaderTest extends TestCase { public function testSupports() @@ -79,7 +84,7 @@ public function testLoadWithImport() $routes = $routeCollection->all(); $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); foreach ($routes as $route) { $this->assertSame('/{foo}/blog/{slug}', $route->getPath()); @@ -176,7 +181,7 @@ public function testLoadLocalized() $routes = $routeCollection->all(); $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); $this->assertEquals('/route', $routeCollection->get('localized.fr')->getPath()); $this->assertEquals('/path', $routeCollection->get('localized.en')->getPath()); @@ -189,7 +194,7 @@ public function testLocalizedImports() $routes = $routeCollection->all(); $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); $this->assertEquals('/le-prefix/le-suffix', $routeCollection->get('imported.fr')->getPath()); $this->assertEquals('/the-prefix/suffix', $routeCollection->get('imported.en')->getPath()); @@ -205,7 +210,7 @@ public function testLocalizedImportsOfNotLocalizedRoutes() $routes = $routeCollection->all(); $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); $this->assertEquals('/le-prefix/suffix', $routeCollection->get('imported.fr')->getPath()); $this->assertEquals('/the-prefix/suffix', $routeCollection->get('imported.en')->getPath()); @@ -214,9 +219,7 @@ public function testLocalizedImportsOfNotLocalizedRoutes() $this->assertSame('en', $routeCollection->get('imported.en')->getRequirement('_locale')); } - /** - * @dataProvider getPathsToInvalidFiles - */ + #[DataProvider('getPathsToInvalidFiles')] public function testLoadThrowsExceptionWithInvalidFile($filePath) { $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); @@ -226,9 +229,7 @@ public function testLoadThrowsExceptionWithInvalidFile($filePath) $loader->load($filePath); } - /** - * @dataProvider getPathsToInvalidFiles - */ + #[DataProvider('getPathsToInvalidFiles')] public function testLoadThrowsExceptionWithInvalidFileEvenWithoutSchemaValidation(string $filePath) { $loader = new CustomXmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); @@ -472,9 +473,7 @@ public function testOverrideControllerInDefaults() $loader->load('override_defaults.xml'); } - /** - * @dataProvider provideFilesImportingRoutesWithControllers - */ + #[DataProvider('provideFilesImportingRoutesWithControllers')] public function testImportRouteWithController(string $file) { $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); @@ -617,9 +616,7 @@ public function testImportingAliases() $this->assertEquals($expectedRoutes('xml'), $routes); } - /** - * @dataProvider providePsr4ConfigFiles - */ + #[DataProvider('providePsr4ConfigFiles')] public function testImportAttributesWithPsr4Prefix(string $configFile) { $locator = new FileLocator(\dirname(__DIR__).'/Fixtures'); diff --git a/Tests/Loader/YamlFileLoaderTest.php b/Tests/Loader/YamlFileLoaderTest.php index 4f6ed3a2..e9ccb966 100644 --- a/Tests/Loader/YamlFileLoaderTest.php +++ b/Tests/Loader/YamlFileLoaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderResolver; @@ -46,9 +47,7 @@ public function testLoadDoesNothingIfEmpty() $this->assertEquals([new FileResource(realpath(__DIR__.'/../Fixtures/empty.yml'))], $collection->getResources()); } - /** - * @dataProvider getPathsToInvalidFiles - */ + #[DataProvider('getPathsToInvalidFiles')] public function testLoadThrowsExceptionWithInvalidFile(string $filePath) { $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); @@ -109,7 +108,7 @@ public function testLoadWithResource() $routes = $routeCollection->all(); $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); foreach ($routes as $route) { $this->assertSame('/{foo}/blog/{slug}', $route->getPath()); @@ -161,9 +160,7 @@ public function testOverrideControllerInDefaults() $loader->load('override_defaults.yml'); } - /** - * @dataProvider provideFilesImportingRoutesWithControllers - */ + #[DataProvider('provideFilesImportingRoutesWithControllers')] public function testImportRouteWithController($file) { $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); @@ -522,9 +519,7 @@ protected function configureRoute( $this->assertSame(1, $routes->getPriority('also_important')); } - /** - * @dataProvider providePsr4ConfigFiles - */ + #[DataProvider('providePsr4ConfigFiles')] public function testImportAttributesWithPsr4Prefix(string $configFile) { $locator = new FileLocator(\dirname(__DIR__).'/Fixtures'); diff --git a/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php b/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php index d6be915a..0068d5bb 100644 --- a/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php +++ b/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Matcher\Dumper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; @@ -47,9 +48,7 @@ public function testRedirectPreservesUrlEncoding() $matcher->match('/foo%3Abar'); } - /** - * @dataProvider getRouteCollections - */ + #[DataProvider('getRouteCollections')] public function testDump(RouteCollection $collection, $fixture) { $basePath = __DIR__.'/../../Fixtures/dumper/'; @@ -439,8 +438,8 @@ public static function getRouteCollections() /* test case 13 */ $hostCollection = new RouteCollection(); - $hostCollection->add('r1', (new Route('abc{foo}'))->setHost('{foo}.exampple.com')); - $hostCollection->add('r2', (new Route('abc{foo}'))->setHost('{foo}.exampple.com')); + $hostCollection->add('r1', (new Route('abc{foo}'))->setHost('{foo}.example.com')); + $hostCollection->add('r2', (new Route('abc{foo}'))->setHost('{foo}.example.com')); /* test case 14 */ $fixedLocaleCollection = new RouteCollection(); @@ -493,7 +492,7 @@ public function testGenerateDumperMatcherWithObject() $routeCollection->add('_', new Route('/', [new \stdClass()])); $dumper = new CompiledUrlMatcherDumper($routeCollection); - $this->expectExceptionMessage('Symfony\Component\Routing\Route cannot contain objects'); + $this->expectExceptionMessage('Symfony\Component\Routing\Route cannot contain objects, but "stdClass" given.'); $this->expectException(\InvalidArgumentException::class); $dumper->dump(); diff --git a/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php b/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php index 9935ced4..939aee5b 100644 --- a/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php +++ b/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php @@ -11,15 +11,14 @@ namespace Symfony\Component\Routing\Tests\Matcher\Dumper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Matcher\Dumper\StaticPrefixCollection; use Symfony\Component\Routing\Route; class StaticPrefixCollectionTest extends TestCase { - /** - * @dataProvider routeProvider - */ + #[DataProvider('routeProvider')] public function testGrouping(array $routes, $expected) { $collection = new StaticPrefixCollection('/'); @@ -44,10 +43,10 @@ public static function routeProvider() ['/leading/segment/', 'leading_segment'], ], << [ [ @@ -56,11 +55,11 @@ public static function routeProvider() ['/prefix/segment/bb', 'leading_segment'], ], << prefix_segment --> leading_segment -EOF, + root + /prefix/segment/ + -> prefix_segment + -> leading_segment + EOF, ], 'Nested - contains item at intersection' => [ [ @@ -69,11 +68,11 @@ public static function routeProvider() ['/prefix/segment/bb', 'leading_segment'], ], << prefix_segment --> leading_segment -EOF, + root + /prefix/segment/ + -> prefix_segment + -> leading_segment + EOF, ], 'Simple one level nesting' => [ [ @@ -83,12 +82,12 @@ public static function routeProvider() ['/group/other/', 'other_segment'], ], << nested_segment --> some_segment --> other_segment -EOF, + root + /group/ + -> nested_segment + -> some_segment + -> other_segment + EOF, ], 'Retain matching order with groups' => [ [ @@ -101,16 +100,16 @@ public static function routeProvider() ['/group/ff/', 'ff'], ], << aa --> bb --> cc -root -/group/ --> dd --> ee --> ff -EOF, + /group/ + -> aa + -> bb + -> cc + root + /group/ + -> dd + -> ee + -> ff + EOF, ], 'Retain complex matching order with groups at base' => [ [ @@ -127,22 +126,22 @@ public static function routeProvider() ['/aaa/333/', 'third_aaa'], ], << first_aaa --> second_aaa --> third_aaa -/prefixed/ --> /prefixed/group/ --> -> aa --> -> bb --> -> cc --> root --> /prefixed/group/ --> -> dd --> -> ee --> -> ff --> parent -EOF, + /aaa/ + -> first_aaa + -> second_aaa + -> third_aaa + /prefixed/ + -> /prefixed/group/ + -> -> aa + -> -> bb + -> -> cc + -> root + -> /prefixed/group/ + -> -> dd + -> -> ee + -> -> ff + -> parent + EOF, ], 'Group regardless of segments' => [ @@ -155,15 +154,15 @@ public static function routeProvider() ['/group-cc/', 'g3'], ], << a1 --> a2 --> a3 -/group- --> g1 --> g2 --> g3 -EOF, + /aaa- + -> a1 + -> a2 + -> a3 + /group- + -> g1 + -> g2 + -> g3 + EOF, ], ]; } diff --git a/Tests/Matcher/ExpressionLanguageProviderTest.php b/Tests/Matcher/ExpressionLanguageProviderTest.php index 71280257..2e5ab7ca 100644 --- a/Tests/Matcher/ExpressionLanguageProviderTest.php +++ b/Tests/Matcher/ExpressionLanguageProviderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Matcher; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; @@ -40,9 +41,7 @@ protected function setUp(): void $this->expressionLanguage->registerProvider(new ExpressionLanguageProvider($functionProvider)); } - /** - * @dataProvider compileProvider - */ + #[DataProvider('compileProvider')] public function testCompile(string $expression, string $expected) { $this->assertSame($expected, $this->expressionLanguage->compile($expression)); @@ -57,9 +56,7 @@ public static function compileProvider(): iterable ]; } - /** - * @dataProvider evaluateProvider - */ + #[DataProvider('evaluateProvider')] public function testEvaluate(string $expression, $expected) { $this->assertSame($expected, $this->expressionLanguage->evaluate($expression, ['context' => $this->context])); diff --git a/Tests/RequestContextTest.php b/Tests/RequestContextTest.php index fcc42ff5..f40045f7 100644 --- a/Tests/RequestContextTest.php +++ b/Tests/RequestContextTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\RequestContext; @@ -27,7 +28,10 @@ public function testConstruct() 8080, 444, '/baz', - 'bar=foobar' + 'bar=foobar', + [ + 'foo' => 'bar', + ] ); $this->assertEquals('foo', $requestContext->getBaseUrl()); @@ -38,6 +42,20 @@ public function testConstruct() $this->assertSame(444, $requestContext->getHttpsPort()); $this->assertEquals('/baz', $requestContext->getPathInfo()); $this->assertEquals('bar=foobar', $requestContext->getQueryString()); + $this->assertSame(['foo' => 'bar'], $requestContext->getParameters()); + } + + public function testConstructParametersBcLayer() + { + $requestContext = new class() extends RequestContext { + public function __construct() + { + $this->setParameters(['foo' => 'bar']); + parent::__construct(); + } + }; + + $this->assertSame(['foo' => 'bar'], $requestContext->getParameters()); } public function testFromUriWithBaseUrl() @@ -85,18 +103,16 @@ public function testFromUriBeingEmpty() $this->assertSame('/', $requestContext->getPathInfo()); } - /** - * @testWith ["http://foo.com\\bar"] - * ["\\\\foo.com/bar"] - * ["a\rb"] - * ["a\nb"] - * ["a\tb"] - * ["\u0000foo"] - * ["foo\u0000"] - * [" foo"] - * ["foo "] - * [":"] - */ + #[TestWith(['http://foo.com\\bar'])] + #[TestWith(['\\\\foo.com/bar'])] + #[TestWith(["a\rb"])] + #[TestWith(["a\nb"])] + #[TestWith(["a\tb"])] + #[TestWith(["\u0000foo"])] + #[TestWith(["foo\u0000"])] + #[TestWith([' foo'])] + #[TestWith(['foo '])] + #[TestWith([':'])] public function testFromBadUri(string $uri) { $context = RequestContext::fromUri($uri); diff --git a/Tests/Requirement/EnumRequirementTest.php b/Tests/Requirement/EnumRequirementTest.php index 68b32ea7..842d4baa 100644 --- a/Tests/Requirement/EnumRequirementTest.php +++ b/Tests/Requirement/EnumRequirementTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Requirement; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Exception\InvalidArgumentException; use Symfony\Component\Routing\Requirement\EnumRequirement; @@ -46,9 +47,7 @@ public function testCaseFromAnotherEnum() new EnumRequirement([TestStringBackedEnum::Diamonds, TestStringBackedEnum2::Spades]); } - /** - * @dataProvider provideToString - */ + #[DataProvider('provideToString')] public function testToString(string $expected, string|array $cases = []) { $this->assertSame($expected, (string) new EnumRequirement($cases)); diff --git a/Tests/Requirement/RequirementTest.php b/Tests/Requirement/RequirementTest.php index d7e0ba07..24c58630 100644 --- a/Tests/Requirement/RequirementTest.php +++ b/Tests/Requirement/RequirementTest.php @@ -11,21 +11,21 @@ namespace Symfony\Component\Routing\Tests\Requirement; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Requirement\Requirement; use Symfony\Component\Routing\Route; class RequirementTest extends TestCase { - /** - * @testWith ["FOO"] - * ["foo"] - * ["1987"] - * ["42-42"] - * ["fo2o-bar"] - * ["foo-bA198r-Ccc"] - * ["fo10O-bar-CCc-fooba187rccc"] - */ + #[TestWith(['FOO'])] + #[TestWith(['foo'])] + #[TestWith(['1987'])] + #[TestWith(['42-42'])] + #[TestWith(['fo2o-bar'])] + #[TestWith(['foo-bA198r-Ccc'])] + #[TestWith(['fo10O-bar-CCc-fooba187rccc'])] public function testAsciiSlugOK(string $slug) { $this->assertMatchesRegularExpression( @@ -34,16 +34,14 @@ public function testAsciiSlugOK(string $slug) ); } - /** - * @testWith [""] - * ["-"] - * ["fôo"] - * ["-FOO"] - * ["foo-"] - * ["-foo-"] - * ["-foo-bar-"] - * ["foo--bar"] - */ + #[TestWith([''])] + #[TestWith(['-'])] + #[TestWith(['fôo'])] + #[TestWith(['-FOO'])] + #[TestWith(['foo-'])] + #[TestWith(['-foo-'])] + #[TestWith(['-foo-bar-'])] + #[TestWith(['foo--bar'])] public function testAsciiSlugKO(string $slug) { $this->assertDoesNotMatchRegularExpression( @@ -52,11 +50,9 @@ public function testAsciiSlugKO(string $slug) ); } - /** - * @testWith ["foo"] - * ["foo/bar/ccc"] - * ["///"] - */ + #[TestWith(['foo'])] + #[TestWith(['foo/bar/ccc'])] + #[TestWith(['///'])] public function testCatchAllOK(string $path) { $this->assertMatchesRegularExpression( @@ -65,9 +61,7 @@ public function testCatchAllOK(string $path) ); } - /** - * @testWith [""] - */ + #[TestWith([''])] public function testCatchAllKO(string $path) { $this->assertDoesNotMatchRegularExpression( @@ -76,13 +70,11 @@ public function testCatchAllKO(string $path) ); } - /** - * @testWith ["0000-01-01"] - * ["9999-12-31"] - * ["2022-04-15"] - * ["2024-02-29"] - * ["1243-04-31"] - */ + #[TestWith(['0000-01-01'])] + #[TestWith(['9999-12-31'])] + #[TestWith(['2022-04-15'])] + #[TestWith(['2024-02-29'])] + #[TestWith(['1243-04-31'])] public function testDateYmdOK(string $date) { $this->assertMatchesRegularExpression( @@ -91,14 +83,12 @@ public function testDateYmdOK(string $date) ); } - /** - * @testWith [""] - * ["foo"] - * ["0000-01-00"] - * ["9999-00-31"] - * ["2022-02-30"] - * ["2022-02-31"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['0000-01-00'])] + #[TestWith(['9999-00-31'])] + #[TestWith(['2022-02-30'])] + #[TestWith(['2022-02-31'])] public function testDateYmdKO(string $date) { $this->assertDoesNotMatchRegularExpression( @@ -107,14 +97,12 @@ public function testDateYmdKO(string $date) ); } - /** - * @testWith ["0"] - * ["012"] - * ["1"] - * ["42"] - * ["42198"] - * ["999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999"] - */ + #[TestWith(['0'])] + #[TestWith(['012'])] + #[TestWith(['1'])] + #[TestWith(['42'])] + #[TestWith(['42198'])] + #[TestWith(['999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999'])] public function testDigitsOK(string $digits) { $this->assertMatchesRegularExpression( @@ -123,12 +111,10 @@ public function testDigitsOK(string $digits) ); } - /** - * @testWith [""] - * ["foo"] - * ["-1"] - * ["3.14"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['-1'])] + #[TestWith(['3.14'])] public function testDigitsKO(string $digits) { $this->assertDoesNotMatchRegularExpression( @@ -137,10 +123,8 @@ public function testDigitsKO(string $digits) ); } - /** - * @testWith ["67c8b7d295c70befc3070bf2"] - * ["000000000000000000000000"] - */ + #[TestWith(['67c8b7d295c70befc3070bf2'])] + #[TestWith(['000000000000000000000000'])] public function testMongoDbIdOK(string $id) { $this->assertMatchesRegularExpression( @@ -149,12 +133,10 @@ public function testMongoDbIdOK(string $id) ); } - /** - * @testWith ["67C8b7D295C70BEFC3070BF2"] - * ["67c8b7d295c70befc3070bg2"] - * ["67c8b7d295c70befc3070bf2a"] - * ["67c8b7d295c70befc3070bf"] - */ + #[TestWith(['67C8b7D295C70BEFC3070BF2'])] + #[TestWith(['67c8b7d295c70befc3070bg2'])] + #[TestWith(['67c8b7d295c70befc3070bf2a'])] + #[TestWith(['67c8b7d295c70befc3070bf'])] public function testMongoDbIdKO(string $id) { $this->assertDoesNotMatchRegularExpression( @@ -163,12 +145,10 @@ public function testMongoDbIdKO(string $id) ); } - /** - * @testWith ["1"] - * ["42"] - * ["42198"] - * ["999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999"] - */ + #[TestWith(['1'])] + #[TestWith(['42'])] + #[TestWith(['42198'])] + #[TestWith(['999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999'])] public function testPositiveIntOK(string $digits) { $this->assertMatchesRegularExpression( @@ -177,14 +157,12 @@ public function testPositiveIntOK(string $digits) ); } - /** - * @testWith [""] - * ["0"] - * ["045"] - * ["foo"] - * ["-1"] - * ["3.14"] - */ + #[TestWith([''])] + #[TestWith(['0'])] + #[TestWith(['045'])] + #[TestWith(['foo'])] + #[TestWith(['-1'])] + #[TestWith(['3.14'])] public function testPositiveIntKO(string $digits) { $this->assertDoesNotMatchRegularExpression( @@ -193,12 +171,10 @@ public function testPositiveIntKO(string $digits) ); } - /** - * @testWith ["00000000000000000000000000"] - * ["ZZZZZZZZZZZZZZZZZZZZZZZZZZ"] - * ["01G0P4XH09KW3RCF7G4Q57ESN0"] - * ["05CSACM1MS9RB9H5F61BYA146Q"] - */ + #[TestWith(['00000000000000000000000000'])] + #[TestWith(['ZZZZZZZZZZZZZZZZZZZZZZZZZZ'])] + #[TestWith(['01G0P4XH09KW3RCF7G4Q57ESN0'])] + #[TestWith(['05CSACM1MS9RB9H5F61BYA146Q'])] public function testUidBase32OK(string $uid) { $this->assertMatchesRegularExpression( @@ -207,12 +183,10 @@ public function testUidBase32OK(string $uid) ); } - /** - * @testWith [""] - * ["foo"] - * ["01G0P4XH09KW3RCF7G4Q57ESN"] - * ["01G0P4XH09KW3RCF7G4Q57ESNU"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['01G0P4XH09KW3RCF7G4Q57ESN'])] + #[TestWith(['01G0P4XH09KW3RCF7G4Q57ESNU'])] public function testUidBase32KO(string $uid) { $this->assertDoesNotMatchRegularExpression( @@ -221,12 +195,10 @@ public function testUidBase32KO(string $uid) ); } - /** - * @testWith ["1111111111111111111111"] - * ["zzzzzzzzzzzzzzzzzzzzzz"] - * ["1BkPBX6T19U8TUAjBTtgwH"] - * ["1fg491dt8eQpf2TU42o2bY"] - */ + #[TestWith(['1111111111111111111111'])] + #[TestWith(['zzzzzzzzzzzzzzzzzzzzzz'])] + #[TestWith(['1BkPBX6T19U8TUAjBTtgwH'])] + #[TestWith(['1fg491dt8eQpf2TU42o2bY'])] public function testUidBase58OK(string $uid) { $this->assertMatchesRegularExpression( @@ -235,12 +207,10 @@ public function testUidBase58OK(string $uid) ); } - /** - * @testWith [""] - * ["foo"] - * ["1BkPBX6T19U8TUAjBTtgw"] - * ["1BkPBX6T19U8TUAjBTtgwI"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['1BkPBX6T19U8TUAjBTtgw'])] + #[TestWith(['1BkPBX6T19U8TUAjBTtgwI'])] public function testUidBase58KO(string $uid) { $this->assertDoesNotMatchRegularExpression( @@ -249,9 +219,7 @@ public function testUidBase58KO(string $uid) ); } - /** - * @dataProvider provideUidRfc4122 - */ + #[DataProvider('provideUidRfc4122')] public function testUidRfc4122OK(string $uid) { $this->assertMatchesRegularExpression( @@ -260,9 +228,7 @@ public function testUidRfc4122OK(string $uid) ); } - /** - * @dataProvider provideUidRfc4122KO - */ + #[DataProvider('provideUidRfc4122KO')] public function testUidRfc4122KO(string $uid) { $this->assertDoesNotMatchRegularExpression( @@ -271,9 +237,7 @@ public function testUidRfc4122KO(string $uid) ); } - /** - * @dataProvider provideUidRfc4122 - */ + #[DataProvider('provideUidRfc4122')] public function testUidRfc9562OK(string $uid) { $this->assertMatchesRegularExpression( @@ -282,9 +246,7 @@ public function testUidRfc9562OK(string $uid) ); } - /** - * @dataProvider provideUidRfc4122KO - */ + #[DataProvider('provideUidRfc4122KO')] public function testUidRfc9562KO(string $uid) { $this->assertDoesNotMatchRegularExpression( @@ -310,11 +272,9 @@ public static function provideUidRfc4122KO(): iterable yield ['01802c4ec4099f07863cf025ca7766a0']; } - /** - * @testWith ["00000000000000000000000000"] - * ["7ZZZZZZZZZZZZZZZZZZZZZZZZZ"] - * ["01G0P4ZPM69QTD4MM4ENAEA4EW"] - */ + #[TestWith(['00000000000000000000000000'])] + #[TestWith(['7ZZZZZZZZZZZZZZZZZZZZZZZZZ'])] + #[TestWith(['01G0P4ZPM69QTD4MM4ENAEA4EW'])] public function testUlidOK(string $ulid) { $this->assertMatchesRegularExpression( @@ -323,12 +283,10 @@ public function testUlidOK(string $ulid) ); } - /** - * @testWith [""] - * ["foo"] - * ["8ZZZZZZZZZZZZZZZZZZZZZZZZZ"] - * ["01G0P4ZPM69QTD4MM4ENAEA4E"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['8ZZZZZZZZZZZZZZZZZZZZZZZZZ'])] + #[TestWith(['01G0P4ZPM69QTD4MM4ENAEA4E'])] public function testUlidKO(string $ulid) { $this->assertDoesNotMatchRegularExpression( @@ -337,15 +295,13 @@ public function testUlidKO(string $ulid) ); } - /** - * @testWith ["00000000-0000-1000-8000-000000000000"] - * ["ffffffff-ffff-6fff-bfff-ffffffffffff"] - * ["8c670a1c-bc95-11ec-8422-0242ac120002"] - * ["61c86569-e477-3ed9-9e3b-1562edb03277"] - * ["e55a29be-ba25-46e0-a5e5-85b78a6f9a11"] - * ["bad98960-f1a1-530e-9a82-07d0b6c4e62f"] - * ["1ecbc9a8-432d-6b14-af93-715adc3b830c"] - */ + #[TestWith(['00000000-0000-1000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-6fff-bfff-ffffffffffff'])] + #[TestWith(['8c670a1c-bc95-11ec-8422-0242ac120002'])] + #[TestWith(['61c86569-e477-3ed9-9e3b-1562edb03277'])] + #[TestWith(['e55a29be-ba25-46e0-a5e5-85b78a6f9a11'])] + #[TestWith(['bad98960-f1a1-530e-9a82-07d0b6c4e62f'])] + #[TestWith(['1ecbc9a8-432d-6b14-af93-715adc3b830c'])] public function testUuidOK(string $uuid) { $this->assertMatchesRegularExpression( @@ -354,15 +310,13 @@ public function testUuidOK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["01802c74-d78c-b085-0cdf-7cbad87c70a3"] - * ["e55a29be-ba25-46e0-a5e5-85b78a6f9a1"] - * ["e55a29bh-ba25-46e0-a5e5-85b78a6f9a11"] - * ["e55a29beba2546e0a5e585b78a6f9a11"] - * ["21902510-bc96-21ec-8422-0242ac120002"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['01802c74-d78c-b085-0cdf-7cbad87c70a3'])] + #[TestWith(['e55a29be-ba25-46e0-a5e5-85b78a6f9a1'])] + #[TestWith(['e55a29bh-ba25-46e0-a5e5-85b78a6f9a11'])] + #[TestWith(['e55a29beba2546e0a5e585b78a6f9a11'])] + #[TestWith(['21902510-bc96-21ec-8422-0242ac120002'])] public function testUuidKO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -371,13 +325,11 @@ public function testUuidKO(string $uuid) ); } - /** - * @testWith ["00000000-0000-1000-8000-000000000000"] - * ["ffffffff-ffff-1fff-bfff-ffffffffffff"] - * ["21902510-bc96-11ec-8422-0242ac120002"] - * ["a8ff8f60-088e-1099-a09d-53afc49918d1"] - * ["b0ac612c-9117-17a1-901f-53afc49918d1"] - */ + #[TestWith(['00000000-0000-1000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-1fff-bfff-ffffffffffff'])] + #[TestWith(['21902510-bc96-11ec-8422-0242ac120002'])] + #[TestWith(['a8ff8f60-088e-1099-a09d-53afc49918d1'])] + #[TestWith(['b0ac612c-9117-17a1-901f-53afc49918d1'])] public function testUuidV1OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -386,14 +338,12 @@ public function testUuidV1OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["a3674b89-0170-3e30-8689-52939013e39c"] - * ["e0040090-3cb0-4bf9-a868-407770c964f9"] - * ["2e2b41d9-e08c-53d2-b435-818b9c323942"] - * ["2a37b67a-5eaa-6424-b5d6-ffc9ba0f2a13"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['a3674b89-0170-3e30-8689-52939013e39c'])] + #[TestWith(['e0040090-3cb0-4bf9-a868-407770c964f9'])] + #[TestWith(['2e2b41d9-e08c-53d2-b435-818b9c323942'])] + #[TestWith(['2a37b67a-5eaa-6424-b5d6-ffc9ba0f2a13'])] public function testUuidV1KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -402,12 +352,10 @@ public function testUuidV1KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-3000-8000-000000000000"] - * ["ffffffff-ffff-3fff-bfff-ffffffffffff"] - * ["2b3f1427-33b2-30a9-8759-07355007c204"] - * ["c38e7b09-07f7-3901-843d-970b0186b873"] - */ + #[TestWith(['00000000-0000-3000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-3fff-bfff-ffffffffffff'])] + #[TestWith(['2b3f1427-33b2-30a9-8759-07355007c204'])] + #[TestWith(['c38e7b09-07f7-3901-843d-970b0186b873'])] public function testUuidV3OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -416,14 +364,12 @@ public function testUuidV3OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["e24d9c0e-bc98-11ec-9924-53afc49918d1"] - * ["1c240248-7d0b-41a4-9d20-61ad2915a58c"] - * ["4816b668-385b-5a65-808d-bca410f45090"] - * ["1d2f3104-dff6-64c6-92ff-0f74b1d0e2af"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['e24d9c0e-bc98-11ec-9924-53afc49918d1'])] + #[TestWith(['1c240248-7d0b-41a4-9d20-61ad2915a58c'])] + #[TestWith(['4816b668-385b-5a65-808d-bca410f45090'])] + #[TestWith(['1d2f3104-dff6-64c6-92ff-0f74b1d0e2af'])] public function testUuidV3KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -432,12 +378,10 @@ public function testUuidV3KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-4000-8000-000000000000"] - * ["ffffffff-ffff-4fff-bfff-ffffffffffff"] - * ["b8f15bf4-46e2-4757-bbce-11ae83f7a6ea"] - * ["eaf51230-1ce2-40f1-ab18-649212b26198"] - */ + #[TestWith(['00000000-0000-4000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-4fff-bfff-ffffffffffff'])] + #[TestWith(['b8f15bf4-46e2-4757-bbce-11ae83f7a6ea'])] + #[TestWith(['eaf51230-1ce2-40f1-ab18-649212b26198'])] public function testUuidV4OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -446,14 +390,12 @@ public function testUuidV4OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["15baaab2-f310-11d2-9ecf-53afc49918d1"] - * ["acd44dc8-d2cc-326c-9e3a-80a3305a25e8"] - * ["7fc2705f-a8a4-5b31-99a8-890686d64189"] - * ["1ecbc991-3552-6920-998e-efad54178a98"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['15baaab2-f310-11d2-9ecf-53afc49918d1'])] + #[TestWith(['acd44dc8-d2cc-326c-9e3a-80a3305a25e8'])] + #[TestWith(['7fc2705f-a8a4-5b31-99a8-890686d64189'])] + #[TestWith(['1ecbc991-3552-6920-998e-efad54178a98'])] public function testUuidV4KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -462,12 +404,10 @@ public function testUuidV4KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-5000-8000-000000000000"] - * ["ffffffff-ffff-5fff-bfff-ffffffffffff"] - * ["49f4d32c-28b3-5802-8717-a2896180efbd"] - * ["58b3c62e-a7df-5a82-93a6-fbe5fda681c1"] - */ + #[TestWith(['00000000-0000-5000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-5fff-bfff-ffffffffffff'])] + #[TestWith(['49f4d32c-28b3-5802-8717-a2896180efbd'])] + #[TestWith(['58b3c62e-a7df-5a82-93a6-fbe5fda681c1'])] public function testUuidV5OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -476,14 +416,12 @@ public function testUuidV5OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["b99ad578-fdd3-1135-9d3b-53afc49918d1"] - * ["b3ee3071-7a2b-3e17-afdf-6b6aec3acf85"] - * ["2ab4f5a7-6412-46c1-b3ab-1fe1ed391e27"] - * ["135fdd3d-e193-653e-865d-67e88cf12e44"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['b99ad578-fdd3-1135-9d3b-53afc49918d1'])] + #[TestWith(['b3ee3071-7a2b-3e17-afdf-6b6aec3acf85'])] + #[TestWith(['2ab4f5a7-6412-46c1-b3ab-1fe1ed391e27'])] + #[TestWith(['135fdd3d-e193-653e-865d-67e88cf12e44'])] public function testUuidV5KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -492,13 +430,11 @@ public function testUuidV5KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-6000-8000-000000000000"] - * ["ffffffff-ffff-6fff-bfff-ffffffffffff"] - * ["2c51caad-c72f-66b2-b6d7-8766d36c73df"] - * ["17941ebb-48fa-6bfe-9bbd-43929f8784f5"] - * ["1ecbc993-f6c2-67f2-8fbe-295ed594b344"] - */ + #[TestWith(['00000000-0000-6000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-6fff-bfff-ffffffffffff'])] + #[TestWith(['2c51caad-c72f-66b2-b6d7-8766d36c73df'])] + #[TestWith(['17941ebb-48fa-6bfe-9bbd-43929f8784f5'])] + #[TestWith(['1ecbc993-f6c2-67f2-8fbe-295ed594b344'])] public function testUuidV6OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -507,14 +443,12 @@ public function testUuidV6OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["821040f4-7b67-12a3-9770-53afc49918d1"] - * ["802dc245-aaaa-3649-98c6-31c549b0df86"] - * ["92d2e5ad-bc4e-4947-a8d9-77706172ca83"] - * ["6e124559-d260-511e-afdc-e57c7025fed0"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['821040f4-7b67-12a3-9770-53afc49918d1'])] + #[TestWith(['802dc245-aaaa-3649-98c6-31c549b0df86'])] + #[TestWith(['92d2e5ad-bc4e-4947-a8d9-77706172ca83'])] + #[TestWith(['6e124559-d260-511e-afdc-e57c7025fed0'])] public function testUuidV6KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -523,11 +457,9 @@ public function testUuidV6KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-7000-8000-000000000000"] - * ["ffffffff-ffff-7fff-bfff-ffffffffffff"] - * ["01910577-4898-7c47-966e-68d127dde2ac"] - */ + #[TestWith(['00000000-0000-7000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-7fff-bfff-ffffffffffff'])] + #[TestWith(['01910577-4898-7c47-966e-68d127dde2ac'])] public function testUuidV7OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -536,14 +468,12 @@ public function testUuidV7OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["15baaab2-f310-11d2-9ecf-53afc49918d1"] - * ["acd44dc8-d2cc-326c-9e3a-80a3305a25e8"] - * ["7fc2705f-a8a4-5b31-99a8-890686d64189"] - * ["1ecbc991-3552-6920-998e-efad54178a98"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['15baaab2-f310-11d2-9ecf-53afc49918d1'])] + #[TestWith(['acd44dc8-d2cc-326c-9e3a-80a3305a25e8'])] + #[TestWith(['7fc2705f-a8a4-5b31-99a8-890686d64189'])] + #[TestWith(['1ecbc991-3552-6920-998e-efad54178a98'])] public function testUuidV7KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -552,11 +482,9 @@ public function testUuidV7KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-8000-8000-000000000000"] - * ["ffffffff-ffff-8fff-bfff-ffffffffffff"] - * ["01910577-4898-8c47-966e-68d127dde2ac"] - */ + #[TestWith(['00000000-0000-8000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-8fff-bfff-ffffffffffff'])] + #[TestWith(['01910577-4898-8c47-966e-68d127dde2ac'])] public function testUuidV8OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -565,14 +493,12 @@ public function testUuidV8OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["15baaab2-f310-11d2-9ecf-53afc49918d1"] - * ["acd44dc8-d2cc-326c-9e3a-80a3305a25e8"] - * ["7fc2705f-a8a4-5b31-99a8-890686d64189"] - * ["1ecbc991-3552-6920-998e-efad54178a98"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['15baaab2-f310-11d2-9ecf-53afc49918d1'])] + #[TestWith(['acd44dc8-d2cc-326c-9e3a-80a3305a25e8'])] + #[TestWith(['7fc2705f-a8a4-5b31-99a8-890686d64189'])] + #[TestWith(['1ecbc991-3552-6920-998e-efad54178a98'])] public function testUuidV8KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( diff --git a/Tests/RouteCollectionTest.php b/Tests/RouteCollectionTest.php index 16ab7f6f..b4a1b5b5 100644 --- a/Tests/RouteCollectionTest.php +++ b/Tests/RouteCollectionTest.php @@ -105,9 +105,9 @@ public function testAddCollection() public function testAddCollectionWithResources() { $collection = new RouteCollection(); - $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/foo.xml')); + $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/empty.yml')); $collection1 = new RouteCollection(); - $collection1->addResource($foo1 = new FileResource(__DIR__.'/Fixtures/foo1.xml')); + $collection1->addResource($foo1 = new FileResource(__DIR__.'/Fixtures/file_resource.yml')); $collection->addCollection($collection1); $this->assertEquals([$foo, $foo1], $collection->getResources(), '->addCollection() merges resources'); } @@ -176,9 +176,9 @@ public function testAddPrefixOverridesDefaultsAndRequirements() public function testResource() { $collection = new RouteCollection(); - $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/foo.xml')); - $collection->addResource($bar = new FileResource(__DIR__.'/Fixtures/bar.xml')); - $collection->addResource(new FileResource(__DIR__.'/Fixtures/foo.xml')); + $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/empty.yml')); + $collection->addResource($bar = new FileResource(__DIR__.'/Fixtures/file_resource.yml')); + $collection->addResource(new FileResource(__DIR__.'/Fixtures/empty.yml')); $this->assertEquals([$foo, $bar], $collection->getResources(), '->addResource() adds a resource and getResources() only returns unique ones by comparing the string representation'); diff --git a/Tests/RouteCompilerTest.php b/Tests/RouteCompilerTest.php index 0a756593..51bc47ed 100644 --- a/Tests/RouteCompilerTest.php +++ b/Tests/RouteCompilerTest.php @@ -11,15 +11,14 @@ namespace Symfony\Component\Routing\Tests; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCompiler; class RouteCompilerTest extends TestCase { - /** - * @dataProvider provideCompileData - */ + #[DataProvider('provideCompileData')] public function testCompile($name, $arguments, $prefix, $regex, $variables, $tokens) { $r = new \ReflectionClass(Route::class); @@ -183,9 +182,7 @@ public static function provideCompileData() ]; } - /** - * @dataProvider provideCompileImplicitUtf8Data - */ + #[DataProvider('provideCompileImplicitUtf8Data')] public function testCompileImplicitUtf8Data($name, $arguments, $prefix, $regex, $variables, $tokens) { $this->expectException(\LogicException::class); @@ -205,39 +202,47 @@ public static function provideCompileImplicitUtf8Data() [ 'Static UTF-8 route', ['/foé'], - '/foé', '{^/foé$}sDu', [], [ + '/foé', + '{^/foé$}sDu', + [], + [ ['text', '/foé'], ], - 'patterns', ], [ 'Route with an implicit UTF-8 requirement', ['/{bar}', ['bar' => null], ['bar' => 'é']], - '', '{^/(?Pé)?$}sDu', ['bar'], [ + '', + '{^/(?Pé)?$}sDu', + ['bar'], + [ ['variable', '/', 'é', 'bar', true], ], - 'requirements', ], [ 'Route with a UTF-8 class requirement', ['/{bar}', ['bar' => null], ['bar' => '\pM']], - '', '{^/(?P\pM)?$}sDu', ['bar'], [ + '', + '{^/(?P\pM)?$}sDu', + ['bar'], + [ ['variable', '/', '\pM', 'bar', true], ], - 'requirements', ], [ 'Route with a UTF-8 separator', ['/foo/{bar}§{_format}', [], [], ['compiler_class' => Utf8RouteCompiler::class]], - '/foo', '{^/foo/(?P[^/§]++)§(?P<_format>[^/]++)$}sDu', ['bar', '_format'], [ + '/foo', + '{^/foo/(?P[^/§]++)§(?P<_format>[^/]++)$}sDu', + ['bar', '_format'], + [ ['variable', '§', '[^/]++', '_format', true], ['variable', '/', '[^/§]++', 'bar', true], ['text', '/foo'], ], - 'patterns', ], ]; } @@ -277,9 +282,7 @@ public function testRouteWithFragmentAsPathParameter() $route->compile(); } - /** - * @dataProvider getVariableNamesStartingWithADigit - */ + #[DataProvider('getVariableNamesStartingWithADigit')] public function testRouteWithVariableNameStartingWithADigit(string $name) { $this->expectException(\DomainException::class); @@ -296,9 +299,7 @@ public static function getVariableNamesStartingWithADigit() ]; } - /** - * @dataProvider provideCompileWithHostData - */ + #[DataProvider('provideCompileWithHostData')] public function testCompileWithHost(string $name, array $arguments, string $prefix, string $regex, array $variables, array $pathVariables, array $tokens, string $hostRegex, array $hostVariables, array $hostTokens) { $r = new \ReflectionClass(Route::class); @@ -376,9 +377,7 @@ public function testRouteWithTooLongVariableName() $route->compile(); } - /** - * @dataProvider provideRemoveCapturingGroup - */ + #[DataProvider('provideRemoveCapturingGroup')] public function testRemoveCapturingGroup(string $regex, string $requirement) { $route = new Route('/{foo}', [], ['foo' => $requirement]); diff --git a/Tests/RouteTest.php b/Tests/RouteTest.php index 34728042..cc67c18c 100644 --- a/Tests/RouteTest.php +++ b/Tests/RouteTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\CompiledRoute; use Symfony\Component\Routing\Route; @@ -141,9 +142,7 @@ public function testRequirementAlternativeStartAndEndRegexSyntax() $this->assertTrue($route->hasRequirement('foo')); } - /** - * @dataProvider getInvalidRequirements - */ + #[DataProvider('getInvalidRequirements')] public function testSetInvalidRequirement($req) { $route = new Route('/{foo}'); @@ -226,9 +225,7 @@ public function testSerialize() $this->assertNotSame($route, $unserialized); } - /** - * @dataProvider provideInlineDefaultAndRequirementCases - */ + #[DataProvider('provideInlineDefaultAndRequirementCases')] public function testInlineDefaultAndRequirement(Route $route, string $expectedPath, string $expectedHost, array $expectedDefaults, array $expectedRequirements) { self::assertSame($expectedPath, $route->getPath()); @@ -323,9 +320,7 @@ public function testSerializedRepresentationKeepsWorking() $this->assertNotSame($route, $unserialized); } - /** - * @dataProvider provideNonLocalizedRoutes - */ + #[DataProvider('provideNonLocalizedRoutes')] public function testLocaleDefaultWithNonLocalizedRoutes(Route $route) { $this->assertNotSame('fr', $route->getDefault('_locale')); @@ -333,9 +328,7 @@ public function testLocaleDefaultWithNonLocalizedRoutes(Route $route) $this->assertSame('fr', $route->getDefault('_locale')); } - /** - * @dataProvider provideLocalizedRoutes - */ + #[DataProvider('provideLocalizedRoutes')] public function testLocaleDefaultWithLocalizedRoutes(Route $route) { $expected = $route->getDefault('_locale'); @@ -345,9 +338,7 @@ public function testLocaleDefaultWithLocalizedRoutes(Route $route) $this->assertSame($expected, $route->getDefault('_locale')); } - /** - * @dataProvider provideNonLocalizedRoutes - */ + #[DataProvider('provideNonLocalizedRoutes')] public function testLocaleRequirementWithNonLocalizedRoutes(Route $route) { $this->assertNotSame('fr', $route->getRequirement('_locale')); @@ -355,9 +346,7 @@ public function testLocaleRequirementWithNonLocalizedRoutes(Route $route) $this->assertSame('fr', $route->getRequirement('_locale')); } - /** - * @dataProvider provideLocalizedRoutes - */ + #[DataProvider('provideLocalizedRoutes')] public function testLocaleRequirementWithLocalizedRoutes(Route $route) { $expected = $route->getRequirement('_locale'); diff --git a/composer.json b/composer.json index 59e30bef..1fcc24b6 100644 --- a/composer.json +++ b/composer.json @@ -20,11 +20,11 @@ "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/config": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", "psr/log": "^1|^2|^3" }, "conflict": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 587ee4c0..6d89fd81 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,11 @@ @@ -18,7 +19,7 @@ - + ./ @@ -26,5 +27,9 @@ ./Tests ./vendor - + + + + +