diff --git a/.github/workflows/phpunit-bridge.yml b/.github/workflows/phpunit-bridge.yml index 56360c4c3d80a..5de320ee91c0e 100644 --- a/.github/workflows/phpunit-bridge.yml +++ b/.github/workflows/phpunit-bridge.yml @@ -35,4 +35,4 @@ jobs: php-version: "7.2" - name: Lint - run: find ./src/Symfony/Bridge/PhpUnit -name '*.php' | grep -v -e /Tests/ -e /Attribute/ -e /Extension/ -e /Metadata/ -e ForV7 -e ForV8 -e ForV9 -e ConstraintLogicTrait | parallel -j 4 php -l {} + run: find ./src/Symfony/Bridge/PhpUnit -name '*.php' | grep -v -e /Tests/ -e /Attribute/ -e /Extension/ -e /Metadata/ -e ForV7 -e ForV8 -e ForV9 -e ConstraintLogicTrait -e SymfonyExtension | parallel -j 4 php -l {} diff --git a/CHANGELOG-7.3.md b/CHANGELOG-7.3.md index b88c6a58f068c..91adc8732cd35 100644 --- a/CHANGELOG-7.3.md +++ b/CHANGELOG-7.3.md @@ -7,6 +7,36 @@ in 7.3 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v7.3.0...v7.3.1 +* 7.3.0-RC1 (2025-05-25) + + * bug #60529 [AssetMapper] Fix SequenceParser possible infinite loop (smnandre) + * bug #60532 [Routing] Fix inline default `null` (HypeMC) + * bug #60535 [DoctrineBridge] Fix resetting the manager when using native lazy objects (HypeMC) + * bug #60500 [PhpUnitBridge] Fix cleaning up mocked features with attributes (HypeMC) + * bug #60330 [FrameworkBundle] skip messenger deduplication middleware registration when no "default" lock is configured (lyrixx) + * bug #60494 [Messenger] fix: Add argument as integer (overexpOG) + * bug #60524 [Notifier] Fix Clicksend transport (BafS) + * bug #60479 [FrameworkBundle] object mapper service definition without form (soyuka) + * bug #60478 [Validator] add missing `$extensions` and `$extensionsMessage` to the `Image` constraint (xabbuh) + * bug #60491 [ObjectMapper] added earlier skip to allow if=false when using source mapping (maciekpaprocki) + * bug #60484 [PhpUnitBridge] Clean up mocked features only when ``@group`` is present (HypeMC) + * bug #60490 [PhpUnitBridge] set path to the PHPUnit autoload file (xabbuh) + * bug #60489 [FrameworkBundle] Fix activation strategy of traceable decorators (nicolas-grekas) + * feature #60475 [Validator] Revert Slug constraint (wouterj) + * feature #60105 [JsonPath] Add `JsonPathAssertionsTrait` and related constraints (alexandre-daubois) + * bug #60423 [DependencyInjection] Make `DefinitionErrorExceptionPass` consider `IGNORE_ON_UNINITIALIZED_REFERENCE` and `RUNTIME_EXCEPTION_ON_INVALID_REFERENCE` the same (MatTheCat) + * bug #60439 [FrameworkBundle] Fix declaring field-attr tags in xml config files (nicolas-grekas) + * bug #60428 [DependencyInjection] Fix missing binding for ServiceCollectionInterface when declaring a service subscriber (nicolas-grekas) + * bug #60426 [Validator] let the `SlugValidator` accept `AsciiSlugger` results (xabbuh) + * bug #60421 [VarExporter] Fixed lazy-loading ghost objects generation with property hooks (cheack) + * bug #60419 [SecurityBundle] normalize string values to a single ExposeSecurityLevel instance (xabbuh) + * bug #60266 [Security] Exclude remember_me from default login authenticators (santysisi) + * bug #60407 [Console] Invokable command `#[Option]` adjustments (kbond) + * bug #60400 [Config] Fix generated comment for multiline "info" (GromNaN) + * bug #60260 [Serializer] Prevent `Cannot traverse an already closed generator` error by materializing Traversable input (santysisi) + * bug #60292 [HttpFoundation] Encode path in `X-Accel-Redirect` header (Athorcis) + * bug #60401 Passing more than one Security attribute is not supported (santysisi) + * 7.3.0-BETA2 (2025-05-10) * bug #58643 [SecurityBundle] Use Composer `InstalledVersions` to check if flex is installed (andyexeter) diff --git a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php index a533b3bb8d12c..fa4d88b99455d 100644 --- a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php +++ b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php @@ -80,21 +80,35 @@ function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) { return; } - try { - $r->resetAsLazyProxy($manager, \Closure::bind( - function () use ($name) { - $name = $this->aliases[$name] ?? $name; + $asProxy = $r->initializeLazyObject($manager) !== $manager; + $initializer = \Closure::bind( + function ($manager) use ($name, $asProxy) { + $name = $this->aliases[$name] ?? $name; + if ($asProxy) { + $manager = false; + } + + $manager = match (true) { + isset($this->fileMap[$name]) => $this->load($this->fileMap[$name], $manager), + !$method = $this->methodMap[$name] ?? null => throw new \LogicException(\sprintf('The "%s" service is synthetic and cannot be reset.', $name)), + (new \ReflectionMethod($this, $method))->isStatic() => $this->{$method}($this, $manager), + default => $this->{$method}($manager), + }; + + if ($asProxy) { + return $manager; + } + }, + $this->container, + Container::class + ); - return match (true) { - isset($this->fileMap[$name]) => $this->load($this->fileMap[$name], false), - !$method = $this->methodMap[$name] ?? null => throw new \LogicException(\sprintf('The "%s" service is synthetic and cannot be reset.', $name)), - (new \ReflectionMethod($this, $method))->isStatic() => $this->{$method}($this, false), - default => $this->{$method}(false), - }; - }, - $this->container, - Container::class - )); + try { + if ($asProxy) { + $r->resetAsLazyProxy($manager, $initializer); + } else { + $r->resetAsLazyGhost($manager, $initializer); + } } catch (\Error $e) { if (__FILE__ !== $e->getFile()) { throw $e; diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DummyManager.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DummyManager.php index 04e5a2acdd334..806ef032d8d5c 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DummyManager.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DummyManager.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bridge\Doctrine\Tests\Fixtures; use Doctrine\Persistence\Mapping\ClassMetadata; @@ -11,6 +20,10 @@ class DummyManager implements ObjectManager { public $bar; + public function __construct() + { + } + public function find($className, $id): ?object { } diff --git a/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php b/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php index fa44ba0a00bbb..4803e6acaf0af 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php @@ -22,7 +22,7 @@ class ManagerRegistryTest extends TestCase { - public static function setUpBeforeClass(): void + public function testResetService() { $container = new ContainerBuilder(); @@ -32,10 +32,7 @@ public static function setUpBeforeClass(): void $dumper = new PhpDumper($container); eval('?>'.$dumper->dump(['class' => 'LazyServiceDoctrineBridgeContainer'])); - } - public function testResetService() - { $container = new \LazyServiceDoctrineBridgeContainer(); $registry = new TestManagerRegistry('name', [], ['defaultManager' => 'foo'], 'defaultConnection', 'defaultManager', 'proxyInterfaceName'); @@ -52,6 +49,63 @@ public function testResetService() $this->assertFalse(isset($foo->bar)); } + /** + * @requires PHP 8.4 + * + * @dataProvider provideResetServiceWithNativeLazyObjectsCases + */ + public function testResetServiceWithNativeLazyObjects(string $class) + { + $container = new $class(); + + $registry = new TestManagerRegistry( + 'irrelevant', + [], + ['defaultManager' => 'foo'], + 'irrelevant', + 'defaultManager', + 'irrelevant', + ); + $registry->setTestContainer($container); + + $foo = $container->get('foo'); + self::assertSame(DummyManager::class, $foo::class); + + $foo->bar = 123; + self::assertTrue(isset($foo->bar)); + + $registry->resetManager(); + + self::assertSame($foo, $container->get('foo')); + self::assertSame(DummyManager::class, $foo::class); + self::assertFalse(isset($foo->bar)); + } + + public static function provideResetServiceWithNativeLazyObjectsCases(): iterable + { + $container = new ContainerBuilder(); + + $container->register('foo', DummyManager::class)->setPublic(true); + $container->getDefinition('foo')->setLazy(true); + $container->compile(); + + $dumper = new PhpDumper($container); + + eval('?>'.$dumper->dump(['class' => 'NativeLazyServiceDoctrineBridgeContainer'])); + + yield ['NativeLazyServiceDoctrineBridgeContainer']; + + $dumps = $dumper->dump(['class' => 'NativeLazyServiceDoctrineBridgeContainerAsFiles', 'as_files' => true]); + + $lastDump = array_pop($dumps); + foreach (array_reverse($dumps) as $dump) { + eval('?>'.$dump); + } + eval('?>'.$lastDump); + + yield ['NativeLazyServiceDoctrineBridgeContainerAsFiles']; + } + /** * When performing an entity manager lazy service reset, the reset operations may re-use the container * to create a "fresh" service: when doing so, it can happen that the "fresh" service is itself a proxy. diff --git a/src/Symfony/Bridge/Doctrine/Tests/Types/DatePointTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Types/DatePointTypeTest.php index 6900de3f168b9..84b265ed6502c 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Types/DatePointTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Types/DatePointTypeTest.php @@ -76,6 +76,14 @@ public function testDateTimeImmutableConvertsToPHPValue() $this->assertSame($expected->format($format), $actual->format($format)); } + public function testDatabaseValueConvertsToPHPValue() + { + $actual = $this->type->convertToPHPValue('2025-03-03 12:13:14', new PostgreSQLPlatform()); + + $this->assertInstanceOf(DatePoint::class, $actual); + $this->assertSame('2025-03-03 12:13:14', $actual->format('Y-m-d H:i:s')); + } + public function testGetName() { $this->assertSame('date_point', $this->type->getName()); diff --git a/src/Symfony/Bridge/PhpUnit/SymfonyExtension.php b/src/Symfony/Bridge/PhpUnit/SymfonyExtension.php index ea87c3257ec16..05ff99aa8aedc 100644 --- a/src/Symfony/Bridge/PhpUnit/SymfonyExtension.php +++ b/src/Symfony/Bridge/PhpUnit/SymfonyExtension.php @@ -11,6 +11,8 @@ namespace Symfony\Bridge\PhpUnit; +use PHPUnit\Event\Code\Test; +use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Test\BeforeTestMethodErrored; use PHPUnit\Event\Test\BeforeTestMethodErroredSubscriber; use PHPUnit\Event\Test\Errored; @@ -19,10 +21,13 @@ use PHPUnit\Event\Test\FinishedSubscriber; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\SkippedSubscriber; +use PHPUnit\Metadata\Group; use PHPUnit\Runner\Extension\Extension; use PHPUnit\Runner\Extension\Facade; use PHPUnit\Runner\Extension\ParameterCollection; use PHPUnit\TextUI\Configuration\Configuration; +use Symfony\Bridge\PhpUnit\Attribute\DnsSensitive; +use Symfony\Bridge\PhpUnit\Attribute\TimeSensitive; use Symfony\Bridge\PhpUnit\Extension\EnableClockMockSubscriber; use Symfony\Bridge\PhpUnit\Extension\RegisterClockMockSubscriber; use Symfony\Bridge\PhpUnit\Extension\RegisterDnsMockSubscriber; @@ -47,34 +52,55 @@ public function bootstrap(Configuration $configuration, Facade $facade, Paramete $facade->registerSubscriber(new RegisterClockMockSubscriber($reader)); $facade->registerSubscriber(new EnableClockMockSubscriber($reader)); - $facade->registerSubscriber(new class implements ErroredSubscriber { + $facade->registerSubscriber(new class($reader) implements ErroredSubscriber { + public function __construct(private AttributeReader $reader) + { + } + public function notify(Errored $event): void { - SymfonyExtension::disableClockMock(); - SymfonyExtension::disableDnsMock(); + SymfonyExtension::disableClockMock($event->test(), $this->reader); + SymfonyExtension::disableDnsMock($event->test(), $this->reader); } }); - $facade->registerSubscriber(new class implements FinishedSubscriber { + $facade->registerSubscriber(new class($reader) implements FinishedSubscriber { + public function __construct(private AttributeReader $reader) + { + } + public function notify(Finished $event): void { - SymfonyExtension::disableClockMock(); - SymfonyExtension::disableDnsMock(); + SymfonyExtension::disableClockMock($event->test(), $this->reader); + SymfonyExtension::disableDnsMock($event->test(), $this->reader); } }); - $facade->registerSubscriber(new class implements SkippedSubscriber { + $facade->registerSubscriber(new class($reader) implements SkippedSubscriber { + public function __construct(private AttributeReader $reader) + { + } + public function notify(Skipped $event): void { - SymfonyExtension::disableClockMock(); - SymfonyExtension::disableDnsMock(); + SymfonyExtension::disableClockMock($event->test(), $this->reader); + SymfonyExtension::disableDnsMock($event->test(), $this->reader); } }); if (interface_exists(BeforeTestMethodErroredSubscriber::class)) { - $facade->registerSubscriber(new class implements BeforeTestMethodErroredSubscriber { + $facade->registerSubscriber(new class($reader) implements BeforeTestMethodErroredSubscriber { + public function __construct(private AttributeReader $reader) + { + } + public function notify(BeforeTestMethodErrored $event): void { - SymfonyExtension::disableClockMock(); - SymfonyExtension::disableDnsMock(); + if (method_exists($event, 'test')) { + SymfonyExtension::disableClockMock($event->test(), $this->reader); + SymfonyExtension::disableDnsMock($event->test(), $this->reader); + } else { + ClockMock::withClockMock(false); + DnsMock::withMockedHosts([]); + } } }); } @@ -91,16 +117,38 @@ public function notify(BeforeTestMethodErrored $event): void /** * @internal */ - public static function disableClockMock(): void + public static function disableClockMock(Test $test, AttributeReader $reader): void { - ClockMock::withClockMock(false); + if (self::hasGroup($test, 'time-sensitive', $reader, TimeSensitive::class)) { + ClockMock::withClockMock(false); + } } /** * @internal */ - public static function disableDnsMock(): void + public static function disableDnsMock(Test $test, AttributeReader $reader): void { - DnsMock::withMockedHosts([]); + if (self::hasGroup($test, 'dns-sensitive', $reader, DnsSensitive::class)) { + DnsMock::withMockedHosts([]); + } + } + + /** + * @internal + */ + public static function hasGroup(Test $test, string $groupName, AttributeReader $reader, string $attribute): bool + { + if (!$test instanceof TestMethod) { + return false; + } + + foreach ($test->metadata() as $metadata) { + if ($metadata instanceof Group && $groupName === $metadata->groupName()) { + return true; + } + } + + return [] !== $reader->forClassAndMethod($test->className(), $test->methodName(), $attribute); } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/SymfonyExtensionWithManualRegister.php b/src/Symfony/Bridge/PhpUnit/Tests/SymfonyExtensionWithManualRegister.php new file mode 100644 index 0000000000000..c02d6f1cf64ce --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/SymfonyExtensionWithManualRegister.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ClockMock; +use Symfony\Bridge\PhpUnit\DnsMock; + +class SymfonyExtensionWithManualRegister extends TestCase +{ + public static function setUpBeforeClass(): void + { + ClockMock::register(self::class); + ClockMock::withClockMock(strtotime('2024-05-20 15:30:00')); + + DnsMock::register(self::class); + DnsMock::withMockedHosts([ + 'example.com' => [ + ['type' => 'A', 'ip' => '1.2.3.4'], + ], + ]); + } + + public static function tearDownAfterClass(): void + { + ClockMock::withClockMock(false); + DnsMock::withMockedHosts([]); + } + + public function testDate() + { + self::assertSame('2024-05-20 15:30:00', date('Y-m-d H:i:s')); + } + + public function testGetHostByName() + { + self::assertSame('1.2.3.4', gethostbyname('example.com')); + } + + public function testTime() + { + self::assertSame(1716219000, time()); + } + + public function testDnsGetRecord() + { + self::assertSame([[ + 'host' => 'example.com', + 'class' => 'IN', + 'ttl' => 1, + 'type' => 'A', + 'ip' => '1.2.3.4', + ]], dns_get_record('example.com')); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/symfonyextension.phpt b/src/Symfony/Bridge/PhpUnit/Tests/symfonyextension.phpt index 933352f07eadc..dd26388e71fd6 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/symfonyextension.phpt +++ b/src/Symfony/Bridge/PhpUnit/Tests/symfonyextension.phpt @@ -5,6 +5,8 @@ if (!getenv('SYMFONY_PHPUNIT_VERSION') || version_compare(getenv('SYMFONY_PHPUNI --FILE-- =')) { + $GLOBALS['_composer_autoload_path'] = "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/vendor/autoload.php"; +} + if ($components) { $skippedTests = $_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS'] ?? false; $runningProcs = []; diff --git a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php index 9eeb305aee36c..b06f0a8cedbe4 100644 --- a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php @@ -44,7 +44,7 @@ public function getFunctions(): array /** * Adds a "Link" HTTP header. * - * @param string $rel The relation type (e.g. "preload", "prefetch", "prerender" or "dns-prefetch") + * @param string $rel The relation type (e.g. "preload", "prefetch", or "dns-prefetch") * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") * * @return string The relation URI @@ -115,7 +115,11 @@ public function prefetch(string $uri, array $attributes = []): string } /** - * Indicates to the client that it should prerender this resource . + * Indicates to the client that it should prerender this resource. + * + * This feature is deprecated and superseded by the Speculation Rules API. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/prerender * * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") * diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 4c40455526e57..f4e137f04b980 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -257,6 +257,7 @@ private function addFormSection(ArrayNodeDefinition $rootNode, callable $enableI ->arrayNode('field_attr') ->performNoDeepMerging() ->normalizeKeys(false) + ->useAttributeAsKey('name') ->scalarPrototype()->end() ->defaultValue(['data-controller' => 'csrf-protection']) ->end() @@ -1289,7 +1290,7 @@ private function addPropertyInfoSection(ArrayNodeDefinition $rootNode, callable ->then(function ($v) { $v['property_info']['with_constructor_extractor'] = false; - trigger_deprecation('symfony/framework-bundle', '7.3', 'Not setting the "with_constructor_extractor" option explicitly is deprecated because its default value will change in version 8.0.'); + trigger_deprecation('symfony/framework-bundle', '7.3', 'Not setting the "property_info.with_constructor_extractor" option explicitly is deprecated because its default value will change in version 8.0.'); return $v; }) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 385a4caf38ded..347f3ed653c87 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -569,9 +569,9 @@ public function load(array $configs, ContainerBuilder $container): void $container->removeDefinition('console.command.scheduler_debug'); } - // messenger depends on validation being registered + // messenger depends on validation, and lock being registered if ($messengerEnabled) { - $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $this->readConfigEnabled('validation', $container, $config['validation']), $this->readConfigEnabled('lock', $container, $config['lock'])); + $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $this->readConfigEnabled('validation', $container, $config['validation']), $this->readConfigEnabled('lock', $container, $config['lock']) && ($config['lock']['resources']['default'] ?? false)); } else { $container->removeDefinition('console.command.messenger_consume_messages'); $container->removeDefinition('console.command.messenger_stats'); @@ -642,6 +642,14 @@ public function load(array $configs, ContainerBuilder $container): void $loader->load('mime_type.php'); } + if (ContainerBuilder::willBeAvailable('symfony/object-mapper', ObjectMapperInterface::class, ['symfony/framework-bundle'])) { + $loader->load('object_mapper.php'); + $container->registerForAutoconfiguration(TransformCallableInterface::class) + ->addTag('object_mapper.transform_callable'); + $container->registerForAutoconfiguration(ConditionCallableInterface::class) + ->addTag('object_mapper.condition_callable'); + } + $container->registerForAutoconfiguration(PackageInterface::class) ->addTag('assets.package'); $container->registerForAutoconfiguration(AssetCompilerInterface::class) @@ -880,14 +888,6 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont if (!ContainerBuilder::willBeAvailable('symfony/translation', Translator::class, ['symfony/framework-bundle', 'symfony/form'])) { $container->removeDefinition('form.type_extension.upload.validator'); } - - if (ContainerBuilder::willBeAvailable('symfony/object-mapper', ObjectMapperInterface::class, ['symfony/framework-bundle'])) { - $loader->load('object_mapper.php'); - $container->registerForAutoconfiguration(TransformCallableInterface::class) - ->addTag('object_mapper.transform_callable'); - $container->registerForAutoconfiguration(ConditionCallableInterface::class) - ->addTag('object_mapper.condition_callable'); - } } private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride): void diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 7c5ba6e39e121..300fe22fb37a9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddDebugLogProcessorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AssetsContextPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass; @@ -202,6 +203,14 @@ public function build(ContainerBuilder $container): void } } + /** + * @internal + */ + public static function considerProfilerEnabled(): bool + { + return !($GLOBALS['app'] ?? null) instanceof Application || empty($_GET) && \in_array('--profile', $_SERVER['argv'] ?? [], true); + } + private function addCompilerPassIfExists(ContainerBuilder $container, string $class, string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, int $priority = 0): void { $container->addResource(new ClassExistenceResource($class)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php index 880adbb908ebf..43e7fb9a5e4cb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php @@ -50,7 +50,6 @@ tagged_iterator('mailer.transport_factory'), ]) - ->set('mailer.default_transport', TransportInterface::class) ->alias('mailer.default_transport', 'mailer.transports') ->alias(TransportInterface::class, 'mailer.default_transport') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php index 68fb295bb8768..a81c53a633461 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Bundle\FrameworkBundle\EventListener\ConsoleProfilerListener; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Component\HttpKernel\Debug\VirtualRequestStack; use Symfony\Component\HttpKernel\EventListener\ProfilerListener; use Symfony\Component\HttpKernel\Profiler\FileProfilerStorage; @@ -61,7 +62,7 @@ ->set('profiler.state_checker', ProfilerStateChecker::class) ->args([ service_locator(['profiler' => service('profiler')->ignoreOnUninitialized()]), - param('kernel.runtime_mode.web'), + inline_service('bool')->factory([FrameworkBundle::class, 'considerProfilerEnabled']), ]) ->set('profiler.is_disabled_state_checker', 'Closure') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 3a6242b837dd3..7f4b48a18b296 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -80,7 +80,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_field_attr.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_field_attr.php new file mode 100644 index 0000000000000..103ee4797a1b8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_field_attr.php @@ -0,0 +1,22 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'csrf_protection' => [ + 'enabled' => true, + ], + 'form' => [ + 'csrf_protection' => [ + 'field-attr' => [ + 'data-foo' => 'bar', + 'data-bar' => 'baz', + ], + ], + ], + 'session' => [ + 'storage_factory_id' => 'session.storage.factory.native', + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_field_attr.xml similarity index 67% rename from src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml rename to src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_field_attr.xml index 4a05e9d33294e..1889703bec2a9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_field_attr.xml @@ -9,7 +9,13 @@ - + + + + bar + baz + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml deleted file mode 100644 index 09ef0ee167eb4..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_field_attr.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_field_attr.yml new file mode 100644 index 0000000000000..db519977548c4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_field_attr.yml @@ -0,0 +1,16 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + csrf_protection: + enabled: true + form: + csrf_protection: + enabled: true + field_attr: + data-foo: bar + data-bar: baz + session: + storage_factory_id: session.storage.factory.native diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 1899d5239eb4d..5ef658693d1a3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -92,6 +92,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Webhook\Client\RequestParser; use Symfony\Component\Webhook\Controller\WebhookController; +use Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass; use Symfony\Component\Workflow\Exception\InvalidDefinitionException; use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; use Symfony\Component\Workflow\WorkflowEvents; @@ -291,7 +292,7 @@ public function testWorkflows() DefinitionValidator::$called = false; $container = $this->createContainerFromFile('workflows', compile: false); - $container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass()); + $container->addCompilerPass(new WorkflowValidatorPass()); $container->compile(); $this->assertTrue($container->hasDefinition('workflow.article'), 'Workflow is registered as a service'); @@ -410,7 +411,7 @@ public function testWorkflowAreValidated() $this->expectException(InvalidDefinitionException::class); $this->expectExceptionMessage('A transition from a place/state must have an unique name. Multiple transitions named "go" from place/state "first" were found on StateMachine "my_workflow".'); $container = $this->createContainerFromFile('workflow_not_valid', compile: false); - $container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass()); + $container->addCompilerPass(new WorkflowValidatorPass()); $container->compile(); } @@ -1490,6 +1491,17 @@ public function testFormsCanBeEnabledWithoutCsrfProtection() $this->assertFalse($container->getParameter('form.type_extension.csrf.enabled')); } + public function testFormCsrfFieldAttr() + { + $container = $this->createContainerFromFile('form_csrf_field_attr'); + + $expected = [ + 'data-foo' => 'bar', + 'data-bar' => 'baz', + ]; + $this->assertSame($expected, $container->getParameter('form.type_extension.csrf.field_attr')); + } + public function testStopwatchEnabledWithDebugModeEnabled() { $container = $this->createContainerFromFile('default_config', [ @@ -2599,6 +2611,14 @@ public function testJsonStreamerEnabled() $this->assertTrue($container->has('json_streamer.stream_writer')); } + public function testObjectMapperEnabled() + { + $container = $this->createContainerFromClosure(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', []); + }); + $this->assertTrue($container->has('object_mapper')); + } + protected function createContainer(array $data = []) { return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index d2bd2b38eb313..f69a53932711c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -20,7 +20,10 @@ use Symfony\Component\RateLimiter\CompoundRateLimiterFactory; use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Workflow\Definition; +use Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass; use Symfony\Component\Workflow\Exception\InvalidDefinitionException; +use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface; class PhpFrameworkExtensionTest extends FrameworkExtensionTestCase { @@ -128,7 +131,7 @@ public function testWorkflowValidationStateMachine() ], ], ]); - $container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass()); + $container->addCompilerPass(new WorkflowValidatorPass()); }); } @@ -456,13 +459,13 @@ public static function emailValidationModeProvider() } } -class WorkflowValidatorWithConstructor implements \Symfony\Component\Workflow\Validator\DefinitionValidatorInterface +class WorkflowValidatorWithConstructor implements DefinitionValidatorInterface { public function __construct(bool $enabled) { } - public function validate(\Symfony\Component\Workflow\Definition $definition, string $name): void + public function validate(Definition $definition, string $name): void { } } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index b3c81b28700a3..15a9496d11067 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -94,6 +94,7 @@ "symfony/mailer": "<6.4", "symfony/messenger": "<6.4", "symfony/mime": "<6.4", + "symfony/object-mapper": ">=7.4", "symfony/property-info": "<6.4", "symfony/property-access": "<6.4", "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", @@ -108,7 +109,7 @@ "symfony/validator": "<6.4", "symfony/web-profiler-bundle": "<6.4", "symfony/webhook": "<7.2", - "symfony/workflow": "<7.3" + "symfony/workflow": "<7.3.0-beta2" }, "autoload": { "psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" }, diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 9b7414de5e532..0a2d32c9f3f4d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -59,6 +59,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->beforeNormalization() ->always() ->then(function ($v) { + if (isset($v['hide_user_not_found']) && isset($v['expose_security_errors'])) { + throw new InvalidConfigurationException('You cannot use both "hide_user_not_found" and "expose_security_errors" at the same time.'); + } + if (isset($v['hide_user_not_found']) && !isset($v['expose_security_errors'])) { $v['expose_security_errors'] = $v['hide_user_not_found'] ? ExposeSecurityLevel::None : ExposeSecurityLevel::All; } @@ -76,7 +80,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->setDeprecated('symfony/security-bundle', '7.3', 'The "%node%" option is deprecated and will be removed in 8.0. Use the "expose_security_errors" option instead.') ->end() ->enumNode('expose_security_errors') - ->beforeNormalization()->ifString()->then(fn ($v) => ['value' => ExposeSecurityLevel::tryFrom($v)])->end() + ->beforeNormalization()->ifString()->then(fn ($v) => ExposeSecurityLevel::tryFrom($v))->end() ->values(ExposeSecurityLevel::cases()) ->defaultValue(ExposeSecurityLevel::None) ->end() diff --git a/src/Symfony/Bundle/SecurityBundle/Security.php b/src/Symfony/Bundle/SecurityBundle/Security.php index 7f311f68d7d2b..2efbb67fc3de0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security.php +++ b/src/Symfony/Bundle/SecurityBundle/Security.php @@ -171,8 +171,7 @@ private function getAuthenticator(?string $authenticatorName, string $firewallNa $firewallAuthenticatorLocator = $this->authenticators[$firewallName]; if (!$authenticatorName) { - $authenticatorIds = array_keys($firewallAuthenticatorLocator->getProvidedServices()); - + $authenticatorIds = array_filter(array_keys($firewallAuthenticatorLocator->getProvidedServices()), fn (string $authenticatorId) => $authenticatorId !== \sprintf('security.authenticator.remember_me.%s', $firewallName)); if (!$authenticatorIds) { throw new LogicException(\sprintf('No authenticator was found for the firewall "%s".', $firewallName)); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php index 6479e56a668e7..6904a21b18113 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php @@ -254,6 +254,9 @@ public static function provideHideUserNotFoundData(): iterable yield [['expose_security_errors' => ExposeSecurityLevel::None], ExposeSecurityLevel::None]; yield [['expose_security_errors' => ExposeSecurityLevel::AccountStatus], ExposeSecurityLevel::AccountStatus]; yield [['expose_security_errors' => ExposeSecurityLevel::All], ExposeSecurityLevel::All]; + yield [['expose_security_errors' => 'none'], ExposeSecurityLevel::None]; + yield [['expose_security_errors' => 'account_status'], ExposeSecurityLevel::AccountStatus]; + yield [['expose_security_errors' => 'all'], ExposeSecurityLevel::All]; } /** @@ -280,4 +283,18 @@ public static function provideHideUserNotFoundLegacyData(): iterable yield [['hide_user_not_found' => true], ExposeSecurityLevel::None, true]; yield [['hide_user_not_found' => false], ExposeSecurityLevel::All, false]; } + + public function testCannotUseHideUserNotFoundAndExposeSecurityErrorsAtTheSameTime() + { + $processor = new Processor(); + $configuration = new MainConfiguration([], []); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('You cannot use both "hide_user_not_found" and "expose_security_errors" at the same time.'); + + $processor->processConfiguration($configuration, [static::$minimalConfig + [ + 'hide_user_not_found' => true, + 'expose_security_errors' => ExposeSecurityLevel::None, + ]]); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php index d4b336b4eaa70..82a444ef10358 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php @@ -152,7 +152,10 @@ public function testLogin() $firewallAuthenticatorLocator ->expects($this->once()) ->method('getProvidedServices') - ->willReturn(['security.authenticator.custom.dev' => $authenticator]) + ->willReturn([ + 'security.authenticator.custom.dev' => $authenticator, + 'security.authenticator.remember_me.main' => $authenticator + ]) ; $firewallAuthenticatorLocator ->expects($this->once()) @@ -252,6 +255,49 @@ public function testLoginWithoutRequestContext() $security->login($user); } + public function testLoginFailsWhenTooManyAuthenticatorsFound() + { + $request = new Request(); + $authenticator = $this->createMock(AuthenticatorInterface::class); + $requestStack = $this->createMock(RequestStack::class); + $firewallMap = $this->createMock(FirewallMap::class); + $firewall = new FirewallConfig('main', 'main'); + $userAuthenticator = $this->createMock(UserAuthenticatorInterface::class); + $user = $this->createMock(UserInterface::class); + $userChecker = $this->createMock(UserCheckerInterface::class); + + $container = $this->createMock(ContainerInterface::class); + $container + ->expects($this->atLeastOnce()) + ->method('get') + ->willReturnMap([ + ['request_stack', $requestStack], + ['security.firewall.map', $firewallMap], + ['security.authenticator.managers_locator', $this->createContainer('main', $userAuthenticator)], + ['security.user_checker_locator', $this->createContainer('main', $userChecker)], + ]) + ; + + $requestStack->expects($this->once())->method('getCurrentRequest')->willReturn($request); + $firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewall); + + $firewallAuthenticatorLocator = $this->createMock(ServiceProviderInterface::class); + $firewallAuthenticatorLocator + ->expects($this->once()) + ->method('getProvidedServices') + ->willReturn([ + 'security.authenticator.custom.main' => $authenticator, + 'security.authenticator.other.main' => $authenticator + ]) + ; + + $security = new Security($container, ['main' => $firewallAuthenticatorLocator]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Too many authenticators were found for the current firewall "main". You must provide an instance of "Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface" to login programmatically. The available authenticators for the firewall "main" are "security.authenticator.custom.main" ,"security.authenticator.other.main'); + $security->login($user); + } + public function testLogout() { $request = new Request(); diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/AttributeExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/AttributeExtensionPass.php index 24f760802bc94..354874866a0ae 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/AttributeExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/AttributeExtensionPass.php @@ -14,10 +14,13 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Twig\Attribute\AsTwigFilter; use Twig\Attribute\AsTwigFunction; use Twig\Attribute\AsTwigTest; +use Twig\Extension\AbstractExtension; use Twig\Extension\AttributeExtension; +use Twig\Extension\ExtensionInterface; /** * Register an instance of AttributeExtension for each service using the @@ -33,6 +36,14 @@ final class AttributeExtensionPass implements CompilerPassInterface public static function autoconfigureFromAttribute(ChildDefinition $definition, AsTwigFilter|AsTwigFunction|AsTwigTest $attribute, \ReflectionMethod $reflector): void { + $class = $reflector->getDeclaringClass(); + if ($class->implementsInterface(ExtensionInterface::class)) { + if ($class->isSubclassOf(AbstractExtension::class)) { + throw new LogicException(\sprintf('The class "%s" cannot extend "%s" and use the "#[%s]" attribute on method "%s()", choose one or the other.', $class->name, AbstractExtension::class, $attribute::class, $reflector->name)); + } + throw new LogicException(\sprintf('The class "%s" cannot implement "%s" and use the "#[%s]" attribute on method "%s()", choose one or the other.', $class->name, ExtensionInterface::class, $attribute::class, $reflector->name)); + } + $definition->addTag(self::TAG); // The service must be tagged as a runtime to call non-static methods diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/AttributeExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Functional/AttributeExtensionTest.php index 81ce2cbe97bca..8b4e4555f36a0 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Functional/AttributeExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/AttributeExtensionTest.php @@ -11,11 +11,15 @@ namespace Symfony\Bundle\TwigBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\After; +use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\Attributes\BeforeClass; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\TwigBundle\Tests\TestCase; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; use Twig\Attribute\AsTwigFilter; @@ -23,23 +27,23 @@ use Twig\Attribute\AsTwigTest; use Twig\Environment; use Twig\Error\RuntimeError; +use Twig\Extension\AbstractExtension; use Twig\Extension\AttributeExtension; class AttributeExtensionTest extends TestCase { - public function testExtensionWithAttributes() + /** @beforeClass */ + #[BeforeClass] + public static function assertTwigVersion(): void { if (!class_exists(AttributeExtension::class)) { self::markTestSkipped('Twig 3.21 is required.'); } + } - $kernel = new class('test', true) extends Kernel - { - public function registerBundles(): iterable - { - return [new FrameworkBundle(), new TwigBundle()]; - } - + public function testExtensionWithAttributes() + { + $kernel = new class extends AttributeExtensionKernel { public function registerContainerConfiguration(LoaderInterface $loader): void { $loader->load(static function (ContainerBuilder $container) { @@ -53,11 +57,6 @@ public function registerContainerConfiguration(LoaderInterface $loader): void $container->setAlias('twig_test', 'twig')->setPublic(true); }); } - - public function getProjectDir(): string - { - return sys_get_temp_dir().'/'.Kernel::VERSION.'/AttributeExtension'; - } }; $kernel->boot(); @@ -73,10 +72,30 @@ public function getProjectDir(): string $twig->getRuntime(StaticExtensionWithAttributes::class); } + public function testInvalidExtensionClass() + { + $kernel = new class extends AttributeExtensionKernel { + public function registerContainerConfiguration(LoaderInterface $loader): void + { + $loader->load(static function (ContainerBuilder $container) { + $container->register(InvalidExtensionWithAttributes::class, InvalidExtensionWithAttributes::class) + ->setAutoconfigured(true); + }); + } + }; + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The class "Symfony\Bundle\TwigBundle\Tests\Functional\InvalidExtensionWithAttributes" cannot extend "Twig\Extension\AbstractExtension" and use the "#[Twig\Attribute\AsTwigFilter]" attribute on method "funFilter()", choose one or the other.'); + + $kernel->boot(); + } + + /** * @before * @after */ + #[Before, After] protected function deleteTempDir() { if (file_exists($dir = sys_get_temp_dir().'/'.Kernel::VERSION.'/AttributeExtension')) { @@ -85,6 +104,24 @@ protected function deleteTempDir() } } +abstract class AttributeExtensionKernel extends Kernel +{ + public function __construct() + { + parent::__construct('test', true); + } + + public function registerBundles(): iterable + { + return [new FrameworkBundle(), new TwigBundle()]; + } + + public function getProjectDir(): string + { + return sys_get_temp_dir().'/'.Kernel::VERSION.'/AttributeExtension'; + } +} + class StaticExtensionWithAttributes { #[AsTwigFilter('foo')] @@ -112,10 +149,19 @@ public function __construct(private bool $prefix) { } - #[AsTwigFilter('foo')] - #[AsTwigFunction('foo')] + #[AsTwigFilter('prefix_foo')] + #[AsTwigFunction('prefix_foo')] public function prefix(string $value): string { return $this->prefix.$value; } } + +class InvalidExtensionWithAttributes extends AbstractExtension +{ + #[AsTwigFilter('fun')] + public function funFilter(): string + { + return 'fun'; + } +} diff --git a/src/Symfony/Component/AssetMapper/Compiler/Parser/JavascriptSequenceParser.php b/src/Symfony/Component/AssetMapper/Compiler/Parser/JavascriptSequenceParser.php index 943c0eea14f51..7531221a8e5ee 100644 --- a/src/Symfony/Component/AssetMapper/Compiler/Parser/JavascriptSequenceParser.php +++ b/src/Symfony/Component/AssetMapper/Compiler/Parser/JavascriptSequenceParser.php @@ -133,36 +133,35 @@ public function parseUntil(int $position): void continue; } - // Single-line string - if ('"' === $matchChar || "'" === $matchChar) { - if (false === $endPos = strpos($this->content, $matchChar, $matchPos + 1)) { - $this->endsWithSequence(self::STATE_STRING, $position); - - return; - } - while (false !== $endPos && '\\' == $this->content[$endPos - 1]) { - $endPos = strpos($this->content, $matchChar, $endPos + 1); + if ('"' === $matchChar || "'" === $matchChar || '`' === $matchChar) { + $endPos = $matchPos + 1; + while (false !== $endPos = strpos($this->content, $matchChar, $endPos)) { + $backslashes = 0; + $i = $endPos - 1; + while ($i >= 0 && $this->content[$i] === '\\') { + $backslashes++; + $i--; + } + + if (0 === $backslashes % 2) { + break; + } + + $endPos++; } - $this->cursor = min($endPos + 1, $position); - $this->setSequence(self::STATE_STRING, $endPos + 1); - continue; - } - - // Multi-line string - if ('`' === $matchChar) { - if (false === $endPos = strpos($this->content, $matchChar, $matchPos + 1)) { + if (false === $endPos) { $this->endsWithSequence(self::STATE_STRING, $position); - return; } - while (false !== $endPos && '\\' == $this->content[$endPos - 1]) { - $endPos = strpos($this->content, $matchChar, $endPos + 1); - } $this->cursor = min($endPos + 1, $position); $this->setSequence(self::STATE_STRING, $endPos + 1); + continue; } + + // Fallback + $this->cursor = $matchPos + 1; } } diff --git a/src/Symfony/Component/AssetMapper/Tests/Compiler/Parser/JavascriptSequenceParserTest.php b/src/Symfony/Component/AssetMapper/Tests/Compiler/Parser/JavascriptSequenceParserTest.php index cd9c88ff72593..794b7bbf61d94 100644 --- a/src/Symfony/Component/AssetMapper/Tests/Compiler/Parser/JavascriptSequenceParserTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/Compiler/Parser/JavascriptSequenceParserTest.php @@ -230,5 +230,10 @@ public static function provideStringCases(): iterable 3, false, ]; + yield 'after unclosed string' => [ + '"hello', + 6, + true, + ]; } } diff --git a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php index 08e91c2d11105..ddd6dbb291051 100644 --- a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php +++ b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php @@ -412,39 +412,39 @@ private function getComment(BaseNode $node): string { $comment = ''; if ('' !== $info = (string) $node->getInfo()) { - $comment .= ' * '.$info."\n"; + $comment .= $info."\n"; } if (!$node instanceof ArrayNode) { foreach ((array) ($node->getExample() ?? []) as $example) { - $comment .= ' * @example '.$example."\n"; + $comment .= '@example '.$example."\n"; } if ('' !== $default = $node->getDefaultValue()) { - $comment .= ' * @default '.(null === $default ? 'null' : var_export($default, true))."\n"; + $comment .= '@default '.(null === $default ? 'null' : var_export($default, true))."\n"; } if ($node instanceof EnumNode) { - $comment .= \sprintf(' * @param ParamConfigurator|%s $value', implode('|', array_unique(array_map(fn ($a) => !$a instanceof \UnitEnum ? var_export($a, true) : '\\'.ltrim(var_export($a, true), '\\'), $node->getValues()))))."\n"; + $comment .= \sprintf('@param ParamConfigurator|%s $value', implode('|', array_unique(array_map(fn ($a) => !$a instanceof \UnitEnum ? var_export($a, true) : '\\'.ltrim(var_export($a, true), '\\'), $node->getValues()))))."\n"; } else { $parameterTypes = $this->getParameterTypes($node); - $comment .= ' * @param ParamConfigurator|'.implode('|', $parameterTypes).' $value'."\n"; + $comment .= '@param ParamConfigurator|'.implode('|', $parameterTypes).' $value'."\n"; } } else { foreach ((array) ($node->getExample() ?? []) as $example) { - $comment .= ' * @example '.json_encode($example)."\n"; + $comment .= '@example '.json_encode($example)."\n"; } if ($node->hasDefaultValue() && [] != $default = $node->getDefaultValue()) { - $comment .= ' * @default '.json_encode($default)."\n"; + $comment .= '@default '.json_encode($default)."\n"; } } if ($node->isDeprecated()) { - $comment .= ' * @deprecated '.$node->getDeprecation($node->getName(), $node->getParent()->getName())['message']."\n"; + $comment .= '@deprecated '.$node->getDeprecation($node->getName(), $node->getParent()->getName())['message']."\n"; } - return $comment; + return $comment ? ' * '.str_replace("\n", "\n * ", rtrim($comment, "\n"))."\n" : ''; } /** diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php index 153af57be9b5b..5c1259c20edd8 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php @@ -38,7 +38,13 @@ public function getConfigTreeBuilder(): TreeBuilder ->arrayPrototype() ->fixXmlConfig('option') ->children() - ->scalarNode('dsn')->end() + ->scalarNode('dsn') + ->info(<<<'INFO' + The DSN to use. This is a required option. + The info is used to describe the DSN, + it can be multi-line. + INFO) + ->end() ->scalarNode('serializer')->defaultNull()->end() ->arrayNode('options') ->normalizeKeys(false) diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php index b9d8b48db3556..6a98166eccc94 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php @@ -16,6 +16,9 @@ class TransportsConfig private $_usedProperties = []; /** + * The DSN to use. This is a required option. + * The info is used to describe the DSN, + * it can be multi-line. * @default null * @param ParamConfigurator|mixed $value * @return $this diff --git a/src/Symfony/Component/Console/Attribute/Argument.php b/src/Symfony/Component/Console/Attribute/Argument.php index 22bfbf48b762d..e6a94d2f10e4c 100644 --- a/src/Symfony/Component/Console/Attribute/Argument.php +++ b/src/Symfony/Component/Console/Attribute/Argument.php @@ -26,6 +26,7 @@ class Argument private string|bool|int|float|array|null $default = null; private array|\Closure $suggestedValues; private ?int $mode = null; + private string $function = ''; /** * Represents a console command definition. @@ -52,17 +53,23 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self return null; } + if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) { + $self->function = $function->class.'::'.$function->name; + } else { + $self->function = $function->name; + } + $type = $parameter->getType(); $name = $parameter->getName(); if (!$type instanceof \ReflectionNamedType) { - throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $name)); + throw new LogicException(\sprintf('The parameter "$%s" of "%s()" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $name, $self->function)); } $parameterTypeName = $type->getName(); if (!\in_array($parameterTypeName, self::ALLOWED_TYPES, true)) { - throw new LogicException(\sprintf('The type "%s" of parameter "$%s" is not supported as a command argument. Only "%s" types are allowed.', $parameterTypeName, $name, implode('", "', self::ALLOWED_TYPES))); + throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command argument. Only "%s" types are allowed.', $parameterTypeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES))); } if (!$self->name) { diff --git a/src/Symfony/Component/Console/Attribute/Option.php b/src/Symfony/Component/Console/Attribute/Option.php index 4aea4831e9ac6..2f0256b177658 100644 --- a/src/Symfony/Component/Console/Attribute/Option.php +++ b/src/Symfony/Component/Console/Attribute/Option.php @@ -22,12 +22,14 @@ class Option { private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array']; + private const ALLOWED_UNION_TYPES = ['bool|string', 'bool|int', 'bool|float']; private string|bool|int|float|array|null $default = null; private array|\Closure $suggestedValues; private ?int $mode = null; private string $typeName = ''; private bool $allowNull = false; + private string $function = ''; /** * Represents a console command --option definition. @@ -56,21 +58,17 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self return null; } - $type = $parameter->getType(); - $name = $parameter->getName(); - - if (!$type instanceof \ReflectionNamedType) { - throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped, Union or Intersection types are not supported for command options.', $name)); + if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) { + $self->function = $function->class.'::'.$function->name; + } else { + $self->function = $function->name; } - $self->typeName = $type->getName(); - - if (!\in_array($self->typeName, self::ALLOWED_TYPES, true)) { - throw new LogicException(\sprintf('The type "%s" of parameter "$%s" is not supported as a command option. Only "%s" types are allowed.', $self->typeName, $name, implode('", "', self::ALLOWED_TYPES))); - } + $name = $parameter->getName(); + $type = $parameter->getType(); if (!$parameter->isDefaultValueAvailable()) { - throw new LogicException(\sprintf('The option parameter "$%s" must declare a default value.', $name)); + throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must declare a default value.', $name, $self->function)); } if (!$self->name) { @@ -80,16 +78,26 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self $self->default = $parameter->getDefaultValue(); $self->allowNull = $parameter->allowsNull(); - if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) { - throw new LogicException(\sprintf('The option parameter "$%s" must not be nullable when it has a default boolean value.', $name)); + if ($type instanceof \ReflectionUnionType) { + return $self->handleUnion($type); + } + + if (!$type instanceof \ReflectionNamedType) { + throw new LogicException(\sprintf('The parameter "$%s" of "%s()" must have a named type. Untyped or Intersection types are not supported for command options.', $name, $self->function)); + } + + $self->typeName = $type->getName(); + + if (!\in_array($self->typeName, self::ALLOWED_TYPES, true)) { + throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES))); } - if ('string' === $self->typeName && null === $self->default) { - throw new LogicException(\sprintf('The option parameter "$%s" must not have a default of null.', $name)); + if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) { + throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must not be nullable when it has a default boolean value.', $name, $self->function)); } - if ('array' === $self->typeName && $self->allowNull) { - throw new LogicException(\sprintf('The option parameter "$%s" must not be nullable.', $name)); + if ($self->allowNull && null !== $self->default) { + throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must either be not-nullable or have a default of null.', $name, $self->function)); } if ('bool' === $self->typeName) { @@ -97,11 +105,10 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self if (false !== $self->default) { $self->mode |= InputOption::VALUE_NEGATABLE; } + } elseif ('array' === $self->typeName) { + $self->mode = InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY; } else { - $self->mode = $self->allowNull ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED; - if ('array' === $self->typeName) { - $self->mode |= InputOption::VALUE_IS_ARRAY; - } + $self->mode = InputOption::VALUE_REQUIRED; } if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $parameter->getDeclaringFunction()->getClosureThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) { @@ -129,6 +136,14 @@ public function resolveValue(InputInterface $input): mixed { $value = $input->getOption($this->name); + if (null === $value && \in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) { + return true; + } + + if ('array' === $this->typeName && $this->allowNull && [] === $value) { + return null; + } + if ('bool' !== $this->typeName) { return $value; } @@ -139,4 +154,28 @@ public function resolveValue(InputInterface $input): mixed return $value ?? $this->default; } + + private function handleUnion(\ReflectionUnionType $type): self + { + $types = array_map( + static fn(\ReflectionType $t) => $t instanceof \ReflectionNamedType ? $t->getName() : null, + $type->getTypes(), + ); + + sort($types); + + $this->typeName = implode('|', array_filter($types)); + + if (!\in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) { + throw new LogicException(\sprintf('The union type for parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types are allowed.', $this->name, $this->function, implode('", "', self::ALLOWED_UNION_TYPES))); + } + + if (false !== $this->default) { + throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must have a default value of false.', $this->name, $this->function)); + } + + $this->mode = InputOption::VALUE_OPTIONAL; + + return $this; + } } diff --git a/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php b/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php index 88f1b78701e0a..5ab7951e7f575 100644 --- a/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php @@ -79,6 +79,7 @@ public function testCommandInputOptionDefinition() #[Option(shortcut: 'v')] bool $verbose = false, #[Option(description: 'User groups')] array $groups = [], #[Option(suggestedValues: [self::class, 'getSuggestedRoles'])] array $roles = ['ROLE_USER'], + #[Option] string|bool $opt = false, ): int { return 0; }); @@ -86,7 +87,8 @@ public function testCommandInputOptionDefinition() $timeoutInputOption = $command->getDefinition()->getOption('idle'); self::assertSame('idle', $timeoutInputOption->getName()); self::assertNull($timeoutInputOption->getShortcut()); - self::assertTrue($timeoutInputOption->isValueOptional()); + self::assertTrue($timeoutInputOption->isValueRequired()); + self::assertFalse($timeoutInputOption->isValueOptional()); self::assertFalse($timeoutInputOption->isNegatable()); self::assertNull($timeoutInputOption->getDefault()); @@ -120,6 +122,14 @@ public function testCommandInputOptionDefinition() self::assertTrue($rolesInputOption->hasCompletion()); $rolesInputOption->complete(new CompletionInput(), $suggestions = new CompletionSuggestions()); self::assertSame(['ROLE_ADMIN', 'ROLE_USER'], array_map(static fn (Suggestion $s) => $s->getValue(), $suggestions->getValueSuggestions())); + + $optInputOption = $command->getDefinition()->getOption('opt'); + self::assertSame('opt', $optInputOption->getName()); + self::assertNull($optInputOption->getShortcut()); + self::assertFalse($optInputOption->isValueRequired()); + self::assertTrue($optInputOption->isValueOptional()); + self::assertFalse($optInputOption->isNegatable()); + self::assertFalse($optInputOption->getDefault()); } public function testInvalidArgumentType() @@ -128,7 +138,6 @@ public function testInvalidArgumentType() $command->setCode(function (#[Argument] object $any) {}); $this->expectException(LogicException::class); - $this->expectExceptionMessage('The type "object" of parameter "$any" is not supported as a command argument. Only "string", "bool", "int", "float", "array" types are allowed.'); $command->getDefinition(); } @@ -136,10 +145,9 @@ public function testInvalidArgumentType() public function testInvalidOptionType() { $command = new Command('foo'); - $command->setCode(function (#[Option] object $any) {}); + $command->setCode(function (#[Option] ?object $any = null) {}); $this->expectException(LogicException::class); - $this->expectExceptionMessage('The type "object" of parameter "$any" is not supported as a command option. Only "string", "bool", "int", "float", "array" types are allowed.'); $command->getDefinition(); } @@ -262,14 +270,30 @@ public function testNonBinaryInputOptions(array $parameters, array $expected) $command = new Command('foo'); $command->setCode(function ( #[Option] string $a = '', - #[Option] ?string $b = '', - #[Option] array $c = [], - #[Option] array $d = ['a', 'b'], + #[Option] array $b = [], + #[Option] array $c = ['a', 'b'], + #[Option] bool|string $d = false, + #[Option] ?string $e = null, + #[Option] ?array $f = null, + #[Option] int $g = 0, + #[Option] ?int $h = null, + #[Option] float $i = 0.0, + #[Option] ?float $j = null, + #[Option] bool|int $k = false, + #[Option] bool|float $l = false, ) use ($expected): int { $this->assertSame($expected[0], $a); $this->assertSame($expected[1], $b); $this->assertSame($expected[2], $c); $this->assertSame($expected[3], $d); + $this->assertSame($expected[4], $e); + $this->assertSame($expected[5], $f); + $this->assertSame($expected[6], $g); + $this->assertSame($expected[7], $h); + $this->assertSame($expected[8], $i); + $this->assertSame($expected[9], $j); + $this->assertSame($expected[10], $k); + $this->assertSame($expected[11], $l); return 0; }); @@ -279,21 +303,29 @@ public function testNonBinaryInputOptions(array $parameters, array $expected) public static function provideNonBinaryInputOptions(): \Generator { - yield 'defaults' => [[], ['', '', [], ['a', 'b']]]; - yield 'with-value' => [['--a' => 'x', '--b' => 'y', '--c' => ['z'], '--d' => ['c', 'd']], ['x', 'y', ['z'], ['c', 'd']]]; - yield 'without-value' => [['--b' => null], ['', null, [], ['a', 'b']]]; + yield 'defaults' => [ + [], + ['', [], ['a', 'b'], false, null, null, 0, null, 0.0, null, false, false], + ]; + yield 'with-value' => [ + ['--a' => 'x', '--b' => ['z'], '--c' => ['c', 'd'], '--d' => 'v', '--e' => 'w', '--f' => ['q'], '--g' => 1, '--h' => 2, '--i' => 3.1, '--j' => 4.2, '--k' => 5, '--l' => 6.3], + ['x', ['z'], ['c', 'd'], 'v', 'w', ['q'], 1, 2, 3.1, 4.2, 5, 6.3], + ]; + yield 'without-value' => [ + ['--d' => null, '--k' => null, '--l' => null], + ['', [], ['a', 'b'], true, null, null, 0, null, 0.0, null, true, true], + ]; } /** * @dataProvider provideInvalidOptionDefinitions */ - public function testInvalidOptionDefinition(callable $code, string $expectedMessage) + public function testInvalidOptionDefinition(callable $code) { $command = new Command('foo'); $command->setCode($code); $this->expectException(LogicException::class); - $this->expectExceptionMessage($expectedMessage); $command->getDefinition(); } @@ -301,24 +333,31 @@ public function testInvalidOptionDefinition(callable $code, string $expectedMess public static function provideInvalidOptionDefinitions(): \Generator { yield 'no-default' => [ - function (#[Option] string $a) {}, - 'The option parameter "$a" must declare a default value.', + function (#[Option] string $a) {} ]; yield 'nullable-bool-default-true' => [ - function (#[Option] ?bool $a = true) {}, - 'The option parameter "$a" must not be nullable when it has a default boolean value.', + function (#[Option] ?bool $a = true) {} ]; yield 'nullable-bool-default-false' => [ - function (#[Option] ?bool $a = false) {}, - 'The option parameter "$a" must not be nullable when it has a default boolean value.', + function (#[Option] ?bool $a = false) {} + ]; + yield 'invalid-union-type' => [ + function (#[Option] array|bool $a = false) {} + ]; + yield 'union-type-cannot-allow-null' => [ + function (#[Option] string|bool|null $a = null) {}, + ]; + yield 'union-type-default-true' => [ + function (#[Option] string|bool $a = true) {}, + ]; + yield 'union-type-default-string' => [ + function (#[Option] string|bool $a = 'foo') {}, ]; - yield 'nullable-string' => [ - function (#[Option] ?string $a = null) {}, - 'The option parameter "$a" must not have a default of null.', + yield 'nullable-string-not-null-default' => [ + function (#[Option] ?string $a = 'foo') {}, ]; - yield 'nullable-array' => [ - function (#[Option] ?array $a = null) {}, - 'The option parameter "$a" must not be nullable.', + yield 'nullable-array-not-null-default' => [ + function (#[Option] ?array $a = []) {}, ]; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php b/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php index 26ab135b1a99c..2d6ad689443e8 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php @@ -62,7 +62,10 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed } if ($value instanceof Reference && $this->currentId !== $targetId = (string) $value) { - if (ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) { + if ( + ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior() + || ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior() + ) { $this->sourceReferences[$targetId][$this->currentId] ??= true; } else { $this->sourceReferences[$targetId][$this->currentId] = false; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php index 87470c39894e4..89b822bc53b44 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php @@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Contracts\Service\Attribute\SubscribedService; +use Symfony\Contracts\Service\ServiceCollectionInterface; use Symfony\Contracts\Service\ServiceProviderInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; @@ -134,6 +135,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed $value->setBindings([ PsrContainerInterface::class => new BoundArgument($locatorRef, false), ServiceProviderInterface::class => new BoundArgument($locatorRef, false), + ServiceCollectionInterface::class => new BoundArgument($locatorRef, false), ] + $value->getBindings()); return parent::processValue($value); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php index 9ab5c27fcf763..5ed7be315114a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php @@ -64,6 +64,9 @@ public function testSkipNestedErrors() $container->register('foo', 'stdClass') ->addArgument(new Reference('bar', ContainerBuilder::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE)); + $container->register('baz', 'stdClass') + ->addArgument(new Reference('bar', ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE)); + $pass = new DefinitionErrorExceptionPass(); $pass->process($container); diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index 2c4d831bb9afa..a7358183bd461 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -237,7 +237,7 @@ public function prepare(Request $request): static $path = $location.substr($path, \strlen($pathPrefix)); // Only set X-Accel-Redirect header if a valid URI can be produced // as nginx does not serve arbitrary file paths. - $this->headers->set($type, $path); + $this->headers->set($type, rawurlencode($path)); $this->maxlen = 0; break; } diff --git a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php index 1263fa39298ff..7627cd5ec492a 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php @@ -314,7 +314,15 @@ public function testXAccelMapping($realpath, $mapping, $virtual) $property->setValue($response, $file); $response->prepare($request); - $this->assertEquals($virtual, $response->headers->get('X-Accel-Redirect')); + $header = $response->headers->get('X-Accel-Redirect'); + + if ($virtual) { + // Making sure the path doesn't contain characters unsupported by nginx + $this->assertMatchesRegularExpression('/^([^?%]|%[0-9A-F]{2})*$/', $header); + $header = rawurldecode($header); + } + + $this->assertEquals($virtual, $header); } public function testDeleteFileAfterSend() @@ -361,6 +369,7 @@ public static function getSampleXAccelMappings() ['/home/Foo/bar.txt', '/var/www/=/files/,/home/Foo/=/baz/', '/baz/bar.txt'], ['/home/Foo/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', '/baz/bar.txt'], ['/tmp/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', null], + ['/var/www/var/www/files/foo%.txt', '/var/www/=/files/', '/files/var/www/files/foo%.txt'], ]; } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index d09c86966dbe2..566e721bf3bb3 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -73,12 +73,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.3.0-BETA2'; + public const VERSION = '7.3.0-RC1'; public const VERSION_ID = 70300; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 3; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'BETA2'; + public const EXTRA_VERSION = 'RC1'; public const END_OF_MAINTENANCE = '05/2025'; public const END_OF_LIFE = '01/2026'; diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathAssertionsTrait.php b/src/Symfony/Component/JsonPath/Test/JsonPathAssertionsTrait.php new file mode 100644 index 0000000000000..42d35339a5760 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathAssertionsTrait.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Assert; +use PHPUnit\Framework\ExpectationFailedException; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +trait JsonPathAssertionsTrait +{ + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathEquals(mixed $expectedValue, JsonPath|string $jsonPath, string $json, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathEquals($jsonPath, $json), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathNotEquals(mixed $expectedValue, JsonPath|string $jsonPath, string $json, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathNotEquals($jsonPath, $json), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathCount(int $expectedCount, JsonPath|string $jsonPath, string $json, string $message = ''): void + { + Assert::assertThat($expectedCount, new JsonPathCount($jsonPath, $json), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathSame(mixed $expectedValue, JsonPath|string $jsonPath, string $json, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathSame($jsonPath, $json), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathNotSame(mixed $expectedValue, JsonPath|string $jsonPath, string $json, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathNotSame($jsonPath, $json), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathContains(mixed $expectedValue, JsonPath|string $jsonPath, string $json, bool $strict = true, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathContains($jsonPath, $json, $strict), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathNotContains(mixed $expectedValue, JsonPath|string $jsonPath, string $json, bool $strict = true, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathNotContains($jsonPath, $json, $strict), $message); + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathContains.php b/src/Symfony/Component/JsonPath/Test/JsonPathContains.php new file mode 100644 index 0000000000000..e043b90a40637 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathContains.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathContains extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + private bool $strict = true, + ) { + } + + public function toString(): string + { + return \sprintf('is found in elements at JSON path "%s"', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + $result = (new JsonCrawler($this->json))->find($this->jsonPath); + + return \in_array($other, $result, $this->strict); + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathCount.php b/src/Symfony/Component/JsonPath/Test/JsonPathCount.php new file mode 100644 index 0000000000000..8c973a8309345 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathCount.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\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathCount extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + ) { + } + + public function toString(): string + { + return \sprintf('matches expected count of JSON path "%s"', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + return $other === \count((new JsonCrawler($this->json))->find($this->jsonPath)); + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathEquals.php b/src/Symfony/Component/JsonPath/Test/JsonPathEquals.php new file mode 100644 index 0000000000000..56825434b5faa --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathEquals.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\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathEquals extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + ) { + } + + public function toString(): string + { + return \sprintf('equals JSON path "%s" result', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + return (new JsonCrawler($this->json))->find($this->jsonPath) == $other; + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathNotContains.php b/src/Symfony/Component/JsonPath/Test/JsonPathNotContains.php new file mode 100644 index 0000000000000..721d60fa29984 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathNotContains.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathNotContains extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + private bool $strict = true, + ) { + } + + public function toString(): string + { + return \sprintf('is not found in elements at JSON path "%s"', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + $result = (new JsonCrawler($this->json))->find($this->jsonPath); + + return !\in_array($other, $result, $this->strict); + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathNotEquals.php b/src/Symfony/Component/JsonPath/Test/JsonPathNotEquals.php new file mode 100644 index 0000000000000..d149dbb59c441 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathNotEquals.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\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathNotEquals extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + ) { + } + + public function toString(): string + { + return \sprintf('does not equal JSON path "%s" result', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + return (new JsonCrawler($this->json))->find($this->jsonPath) != $other; + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathNotSame.php b/src/Symfony/Component/JsonPath/Test/JsonPathNotSame.php new file mode 100644 index 0000000000000..248ac456fcbef --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathNotSame.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\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathNotSame extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + ) { + } + + public function toString(): string + { + return \sprintf('is not identical to JSON path "%s" result', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + return (new JsonCrawler($this->json))->find($this->jsonPath) !== $other; + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathSame.php b/src/Symfony/Component/JsonPath/Test/JsonPathSame.php new file mode 100644 index 0000000000000..469922d8a0b90 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathSame.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\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathSame extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + ) { + } + + public function toString(): string + { + return \sprintf('is identical to JSON path "%s" result', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + return (new JsonCrawler($this->json))->find($this->jsonPath) === $other; + } +} diff --git a/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php b/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php new file mode 100644 index 0000000000000..62d64b53e1e8d --- /dev/null +++ b/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php @@ -0,0 +1,191 @@ +getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathNotEqualsOk() + { + self::assertJsonPathNotEquals([2], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathNotEqualsKo() + { + $thrown = false; + try { + self::assertJsonPathNotEquals([1], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertMatchesRegularExpression('/Failed asserting that .+ does not equal JSON path "\$\.a\[2]" result./s', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathCountOk() + { + self::assertJsonPathCount(6, '$.a[*]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathCountOkWithFilter() + { + self::assertJsonPathCount(2, '$.book[?(@.price > 25)]', <<getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathSameOk() + { + self::assertJsonPathSame([1], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathSameKo() + { + $thrown = false; + try { + self::assertJsonPathSame([2], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertMatchesRegularExpression('/Failed asserting that .+ is identical to JSON path "\$\.a\[2]" result\./s', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathHasNoTypeCoercion() + { + $thrown = false; + try { + self::assertJsonPathSame(['1'], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertMatchesRegularExpression('/Failed asserting that .+ is identical to JSON path "\$\.a\[2]" result\./s', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathNotSameOk() + { + self::assertJsonPathNotSame([2], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathNotSameKo() + { + $thrown = false; + try { + self::assertJsonPathNotSame([1], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertMatchesRegularExpression('/Failed asserting that .+ is not identical to JSON path "\$\.a\[2]" result\./s', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathNotSameHasNoTypeCoercion() + { + self::assertJsonPathNotSame(['1'], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathContainsOk() + { + self::assertJsonPathContains(1, '$.a[*]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathContainsKo() + { + $thrown = false; + try { + self::assertJsonPathContains(0, '$.a[*]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertSame('Failed asserting that 0 is found in elements at JSON path "$.a[*]".', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathNotContainsOk() + { + self::assertJsonPathNotContains(0, '$.a[*]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathNotContainsKo() + { + $thrown = false; + try { + self::assertJsonPathNotContains(1, '$.a[*]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertSame('Failed asserting that 1 is not found in elements at JSON path "$.a[*]".', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + private static function getSimpleCollectionCrawlerData(): string + { + return <<=7.4" }, "autoload": { "psr-4": { "Symfony\\Component\\JsonPath\\": "" }, diff --git a/src/Symfony/Component/JsonStreamer/Mapping/Read/DateTimeTypePropertyMetadataLoader.php b/src/Symfony/Component/JsonStreamer/Mapping/Read/DateTimeTypePropertyMetadataLoader.php index 11ce2b4f93962..26bc022cae2e3 100644 --- a/src/Symfony/Component/JsonStreamer/Mapping/Read/DateTimeTypePropertyMetadataLoader.php +++ b/src/Symfony/Component/JsonStreamer/Mapping/Read/DateTimeTypePropertyMetadataLoader.php @@ -38,7 +38,7 @@ public function load(string $className, array $options = [], array $context = [] $type = $metadata->getType(); if ($type instanceof ObjectType && is_a($type->getClassName(), \DateTimeInterface::class, true)) { - if (\DateTime::class === $type->getClassName()) { + if (is_a($type->getClassName(), \DateTime::class, true)) { throw new InvalidArgumentException('The "DateTime" class is not supported. Use "DateTimeImmutable" instead.'); } diff --git a/src/Symfony/Component/JsonStreamer/Tests/Mapping/Read/DateTimeTypePropertyMetadataLoaderTest.php b/src/Symfony/Component/JsonStreamer/Tests/Mapping/Read/DateTimeTypePropertyMetadataLoaderTest.php index c71189815be29..779499adf21c2 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Mapping/Read/DateTimeTypePropertyMetadataLoaderTest.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Mapping/Read/DateTimeTypePropertyMetadataLoaderTest.php @@ -47,6 +47,18 @@ public function testThrowWhenDateTimeType() $loader->load(self::class); } + public function testThrowWhenDateTimeSubclassType() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "DateTime" class is not supported. Use "DateTimeImmutable" instead.'); + + $loader = new DateTimeTypePropertyMetadataLoader(self::propertyMetadataLoader([ + 'mutable' => new PropertyMetadata('mutable', Type::object(DateTimeChild::class)), + ])); + + $loader->load(self::class); + } + /** * @param array $propertiesMetadata */ @@ -64,3 +76,7 @@ public function load(string $className, array $options = [], array $context = [] }; } } + +class DateTimeChild extends \DateTime +{ +} diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/README.md b/src/Symfony/Component/Mailer/Bridge/Azure/README.md index acd9cc25abb53..36b81fccfa385 100644 --- a/src/Symfony/Component/Mailer/Bridge/Azure/README.md +++ b/src/Symfony/Component/Mailer/Bridge/Azure/README.md @@ -21,8 +21,8 @@ where: Resources --------- - * [Microsoft Azure (ACS) Email API Docs](https://learn.microsoft.com/en-us/rest/api/communication/dataplane/email/send) + * [Microsoft Azure (ACS) Email API Docs](https://learn.microsoft.com/en-us/rest/api/communication/email/email/send) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) \ No newline at end of file + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php index d5a6b666075f7..2599586f8f3d8 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php @@ -31,6 +31,7 @@ class Connection 'x-max-length-bytes', 'x-max-priority', 'x-message-ttl', + 'x-delivery-limit', ]; /** diff --git a/src/Symfony/Component/Messenger/Stamp/DeduplicateStamp.php b/src/Symfony/Component/Messenger/Stamp/DeduplicateStamp.php index 4e08d5369f261..1b9ff480b4f49 100644 --- a/src/Symfony/Component/Messenger/Stamp/DeduplicateStamp.php +++ b/src/Symfony/Component/Messenger/Stamp/DeduplicateStamp.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Stamp; use Symfony\Component\Lock\Key; +use Symfony\Component\Messenger\Exception\LogicException; final class DeduplicateStamp implements StampInterface { @@ -22,6 +23,10 @@ public function __construct( private ?float $ttl = 300.0, private bool $onlyDeduplicateInQueue = false, ) { + if (!class_exists(Key::class)) { + throw new LogicException(\sprintf('You cannot use the "%s" as the Lock component is not installed. Try running "composer require symfony/lock".', self::class)); + } + $this->key = new Key($key); } diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php b/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php index 8d8987e4f7c7a..67af3ac9237a7 100644 --- a/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php @@ -87,7 +87,7 @@ protected function doSend(MessageInterface $message): SentMessage $response = $this->client->request('POST', $endpoint, [ 'auth_basic' => [$this->apiUsername, $this->apiKey], - 'json' => array_filter($options), + 'json' => ['messages' => [array_filter($options)]], ]); try { diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php b/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php index 6afae4409fa57..e1f9fa37dcae0 100644 --- a/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php @@ -63,10 +63,14 @@ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValid(string $from $response = $this->createMock(ResponseInterface::class); $response->expects(self::exactly(2))->method('getStatusCode')->willReturn(200); $response->expects(self::once())->method('getContent')->willReturn(''); - $client = new MockHttpClient(function (string $method, string $url) use ($response): ResponseInterface { + $client = new MockHttpClient(function (string $method, string $url, array $options) use ($response): ResponseInterface { self::assertSame('POST', $method); self::assertSame('https://rest.clicksend.com/v3/sms/send', $url); + $body = json_decode($options['body'], true); + self::assertIsArray($body); + self::assertArrayHasKey('messages', $body); + return $response; }); $transport = $this->createTransport($client, $from); diff --git a/src/Symfony/Component/Notifier/Bridge/Mercure/MercureOptions.php b/src/Symfony/Component/Notifier/Bridge/Mercure/MercureOptions.php index 4f3f80c0d7649..85a513cee6901 100644 --- a/src/Symfony/Component/Notifier/Bridge/Mercure/MercureOptions.php +++ b/src/Symfony/Component/Notifier/Bridge/Mercure/MercureOptions.php @@ -22,6 +22,21 @@ final class MercureOptions implements MessageOptionsInterface /** * @param string|string[]|null $topics + * @param array{ + * badge?: string, + * body?: string, + * data?: mixed, + * dir?: 'auto'|'ltr'|'rtl', + * icon?: string, + * image?: string, + * lang?: string, + * renotify?: bool, + * requireInteraction?: bool, + * silent?: bool, + * tag?: string, + * timestamp?: int, + * vibrate?: int|list, + * }|null $content */ public function __construct( string|array|null $topics = null, @@ -62,6 +77,23 @@ public function getRetry(): ?int return $this->retry; } + /** + * @return array{ + * badge?: string, + * body?: string, + * data?: mixed, + * dir?: 'auto'|'ltr'|'rtl', + * icon?: string, + * image?: string, + * lang?: string, + * renotify?: bool, + * requireInteraction?: bool, + * silent?: bool, + * tag?: string, + * timestamp?: int, + * vibrate?: int|list, + * }|null + */ public function getContent(): ?array { return $this->content; diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json b/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json index 259fb81e9bcae..0b1907fb71f15 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json @@ -31,7 +31,7 @@ "symfony/polyfill-php83": "^1.28" }, "require-dev": { - "symfony/webhook": "^6.4|^7.0|^7.2" + "symfony/webhook": "^6.4|^7.0" }, "autoload": { "psr-4": { diff --git a/src/Symfony/Component/ObjectMapper/ObjectMapper.php b/src/Symfony/Component/ObjectMapper/ObjectMapper.php index 7624a05f7bfe0..d78bc3ce8d216 100644 --- a/src/Symfony/Component/ObjectMapper/ObjectMapper.php +++ b/src/Symfony/Component/ObjectMapper/ObjectMapper.php @@ -122,12 +122,12 @@ public function map(object $source, object|string|null $target = null): object $sourcePropertyName = $mapping->source; } - $value = $this->getRawValue($source, $sourcePropertyName); - if (($if = $mapping->if) && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source, $mappedTarget)) { + if (false === $if = $mapping->if) { continue; } - if (false === $if) { + $value = $this->getRawValue($source, $sourcePropertyName); + if ($if && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source, $mappedTarget)) { continue; } diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/HydrateObject/SourceOnly.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/HydrateObject/SourceOnly.php index 9e3127b80d965..c062427c6e8d0 100644 --- a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/HydrateObject/SourceOnly.php +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/HydrateObject/SourceOnly.php @@ -15,7 +15,9 @@ class SourceOnly { - public function __construct(#[Map(source: 'name')] public string $mappedName) - { + public function __construct( + #[Map(source: 'name')] public string $mappedName, + #[Map(if: false)] public ?string $mappedDescription = null + ) { } } diff --git a/src/Symfony/Component/Routing/Route.php b/src/Symfony/Component/Routing/Route.php index 1ed484f71237b..621a4239fcf7a 100644 --- a/src/Symfony/Component/Routing/Route.php +++ b/src/Symfony/Component/Routing/Route.php @@ -420,7 +420,7 @@ private function extractInlineDefaultsAndRequirements(string $pattern): string $pattern = preg_replace_callback('#\{(!?)([\w\x80-\xFF]++)(:([\w\x80-\xFF]++)(\.[\w\x80-\xFF]++)?)?(<.*?>)?(\?[^\}]*+)?\}#', function ($m) use (&$mapping) { if (isset($m[7][0])) { - $this->setDefault($m[2], '?' !== $m[6] ? substr($m[7], 1) : null); + $this->setDefault($m[2], '?' !== $m[7] ? substr($m[7], 1) : null); } if (isset($m[6][0])) { $this->setRequirement($m[2], substr($m[6], 1, -1)); diff --git a/src/Symfony/Component/Routing/Tests/RouteTest.php b/src/Symfony/Component/Routing/Tests/RouteTest.php index b58358a3ef31b..3472804249f57 100644 --- a/src/Symfony/Component/Routing/Tests/RouteTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteTest.php @@ -226,37 +226,48 @@ public function testSerialize() $this->assertNotSame($route, $unserialized); } - public function testInlineDefaultAndRequirement() + /** + * @dataProvider provideInlineDefaultAndRequirementCases + */ + public function testInlineDefaultAndRequirement(Route $route, string $expectedPath, string $expectedHost, array $expectedDefaults, array $expectedRequirements) + { + self::assertSame($expectedPath, $route->getPath()); + self::assertSame($expectedHost, $route->getHost()); + self::assertSame($expectedDefaults, $route->getDefaults()); + self::assertSame($expectedRequirements, $route->getRequirements()); + } + + public static function provideInlineDefaultAndRequirementCases(): iterable { - $this->assertEquals((new Route('/foo/{bar}'))->setDefault('bar', null), new Route('/foo/{bar?}')); - $this->assertEquals((new Route('/foo/{bar}'))->setDefault('bar', 'baz'), new Route('/foo/{bar?baz}')); - $this->assertEquals((new Route('/foo/{bar}'))->setDefault('bar', 'baz'), new Route('/foo/{bar?baz}')); - $this->assertEquals((new Route('/foo/{!bar}'))->setDefault('bar', 'baz'), new Route('/foo/{!bar?baz}')); - $this->assertEquals((new Route('/foo/{bar}'))->setDefault('bar', 'baz'), new Route('/foo/{bar?}', ['bar' => 'baz'])); - - $this->assertEquals((new Route('/foo/{bar}'))->setRequirement('bar', '.*'), new Route('/foo/{bar<.*>}')); - $this->assertEquals((new Route('/foo/{bar}'))->setRequirement('bar', '>'), new Route('/foo/{bar<>>}')); - $this->assertEquals((new Route('/foo/{bar}'))->setRequirement('bar', '\d+'), new Route('/foo/{bar<.*>}', [], ['bar' => '\d+'])); - $this->assertEquals((new Route('/foo/{bar}'))->setRequirement('bar', '[a-z]{2}'), new Route('/foo/{bar<[a-z]{2}>}')); - $this->assertEquals((new Route('/foo/{!bar}'))->setRequirement('bar', '\d+'), new Route('/foo/{!bar<\d+>}')); - - $this->assertEquals((new Route('/foo/{bar}'))->setDefault('bar', null)->setRequirement('bar', '.*'), new Route('/foo/{bar<.*>?}')); - $this->assertEquals((new Route('/foo/{bar}'))->setDefault('bar', '<>')->setRequirement('bar', '>'), new Route('/foo/{bar<>>?<>}')); - - $this->assertEquals((new Route('/{foo}/{!bar}'))->setDefaults(['bar' => '<>', 'foo' => '\\'])->setRequirements(['bar' => '\\', 'foo' => '.']), new Route('/{foo<.>?\}/{!bar<\>?<>}')); - - $this->assertEquals((new Route('/'))->setHost('{bar}')->setDefault('bar', null), (new Route('/'))->setHost('{bar?}')); - $this->assertEquals((new Route('/'))->setHost('{bar}')->setDefault('bar', 'baz'), (new Route('/'))->setHost('{bar?baz}')); - $this->assertEquals((new Route('/'))->setHost('{bar}')->setDefault('bar', 'baz'), (new Route('/'))->setHost('{bar?baz}')); - $this->assertEquals((new Route('/'))->setHost('{bar}')->setDefault('bar', null), (new Route('/', ['bar' => 'baz']))->setHost('{bar?}')); - - $this->assertEquals((new Route('/'))->setHost('{bar}')->setRequirement('bar', '.*'), (new Route('/'))->setHost('{bar<.*>}')); - $this->assertEquals((new Route('/'))->setHost('{bar}')->setRequirement('bar', '>'), (new Route('/'))->setHost('{bar<>>}')); - $this->assertEquals((new Route('/'))->setHost('{bar}')->setRequirement('bar', '.*'), (new Route('/', [], ['bar' => '\d+']))->setHost('{bar<.*>}')); - $this->assertEquals((new Route('/'))->setHost('{bar}')->setRequirement('bar', '[a-z]{2}'), (new Route('/'))->setHost('{bar<[a-z]{2}>}')); - - $this->assertEquals((new Route('/'))->setHost('{bar}')->setDefault('bar', null)->setRequirement('bar', '.*'), (new Route('/'))->setHost('{bar<.*>?}')); - $this->assertEquals((new Route('/'))->setHost('{bar}')->setDefault('bar', '<>')->setRequirement('bar', '>'), (new Route('/'))->setHost('{bar<>>?<>}')); + yield [new Route('/foo/{bar?}'), '/foo/{bar}', '', ['bar' => null], []]; + yield [new Route('/foo/{bar?baz}'), '/foo/{bar}', '', ['bar' => 'baz'], []]; + yield [new Route('/foo/{bar?baz}'), '/foo/{bar}', '', ['bar' => 'baz'], []]; + yield [new Route('/foo/{!bar?baz}'), '/foo/{!bar}', '', ['bar' => 'baz'], []]; + yield [new Route('/foo/{bar?}', ['bar' => 'baz']), '/foo/{bar}', '', ['bar' => 'baz'], []]; + + yield [new Route('/foo/{bar<.*>}'), '/foo/{bar}', '', [], ['bar' => '.*']]; + yield [new Route('/foo/{bar<>>}'), '/foo/{bar}', '', [], ['bar' => '>']]; + yield [new Route('/foo/{bar<.*>}', [], ['bar' => '\d+']), '/foo/{bar}', '', [], ['bar' => '\d+']]; + yield [new Route('/foo/{bar<[a-z]{2}>}'), '/foo/{bar}', '', [], ['bar' => '[a-z]{2}']]; + yield [new Route('/foo/{!bar<\d+>}'), '/foo/{!bar}', '', [], ['bar' => '\d+']]; + + yield [new Route('/foo/{bar<.*>?}'), '/foo/{bar}', '', ['bar' => null], ['bar' => '.*']]; + yield [new Route('/foo/{bar<>>?<>}'), '/foo/{bar}', '', ['bar' => '<>'], ['bar' => '>']]; + + yield [new Route('/{foo<.>?\}/{!bar<\>?<>}'), '/{foo}/{!bar}', '', ['foo' => '\\', 'bar' => '<>'], ['foo' => '.', 'bar' => '\\']]; + + yield [new Route('/', host: '{bar?}'), '/', '{bar}', ['bar' => null], []]; + yield [new Route('/', host: '{bar?baz}'), '/', '{bar}', ['bar' => 'baz'], []]; + yield [new Route('/', host: '{bar?baz}'), '/', '{bar}', ['bar' => 'baz'], []]; + yield [new Route('/', ['bar' => 'baz'], host: '{bar?}'), '/', '{bar}', ['bar' => null], []]; + + yield [new Route('/', host: '{bar<.*>}'), '/', '{bar}', [], ['bar' => '.*']]; + yield [new Route('/', host: '{bar<>>}'), '/', '{bar}', [], ['bar' => '>']]; + yield [new Route('/', [], ['bar' => '\d+'], host: '{bar<.*>}'), '/', '{bar}', [], ['bar' => '.*']]; + yield [new Route('/', host: '{bar<[a-z]{2}>}'), '/', '{bar}', [], ['bar' => '[a-z]{2}']]; + + yield [new Route('/', host: '{bar<.*>?}'), '/', '{bar}', ['bar' => null], ['bar' => '.*']]; + yield [new Route('/', host: '{bar<>>?<>}'), '/', '{bar}', ['bar' => '<>'], ['bar' => '>']]; } /** diff --git a/src/Symfony/Component/Security/Core/Authorization/TraceableAccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/TraceableAccessDecisionManager.php index a03e2d0ca749b..0ef062f6cc37d 100644 --- a/src/Symfony/Component/Security/Core/Authorization/TraceableAccessDecisionManager.php +++ b/src/Symfony/Component/Security/Core/Authorization/TraceableAccessDecisionManager.php @@ -54,7 +54,7 @@ public function decide(TokenInterface $token, array $attributes, mixed $object = $this->accessDecisionStack[] = $accessDecision; try { - return $accessDecision->isGranted = $this->manager->decide($token, $attributes, $object, $accessDecision); + return $accessDecision->isGranted = $this->manager->decide($token, $attributes, $object, $accessDecision, $allowMultipleAttributes); } finally { $this->strategy = $accessDecision->strategy; $currentLog = array_pop($this->currentLog); diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.be.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.be.xlf index 194392935fcc1..6478e2a15caf7 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.be.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.be.xlf @@ -76,7 +76,7 @@ Too many failed login attempts, please try again in %minutes% minutes. - Занадта шмат няўдалых спробаў уваходу, калі ласка, паспрабуйце зноў праз %minutes% хвіліну.|Занадта шмат няўдалых спробаў уваходу, калі ласка, паспрабуйце зноў праз %minutes% хвіліны.|Занадта шмат няўдалых спробаў уваходу, калі ласка, паспрабуйце зноў праз %minutes% хвілін. + Занадта вялікая колькасць няўдалых спробаў уваходу. Калі ласка, паспрабуйце зноў праз %minutes% хвіліну.|Занадта вялікая колькасць няўдалых спробаў уваходу. Калі ласка, паспрабуйце зноў праз %minutes% хвіліны.|Занадта вялікая колькасць няўдалых спробаў уваходу. Калі ласка, паспрабуйце зноў праз %minutes% хвілін. diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php index f5313bb541c22..496d970cd1f00 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php @@ -11,12 +11,14 @@ namespace Symfony\Component\Security\Core\Tests\Authorization; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; use Symfony\Component\Security\Core\Tests\Fixtures\DummyVoter; class TraceableAccessDecisionManagerTest extends TestCase @@ -276,4 +278,48 @@ public function testCustomAccessDecisionManagerReturnsEmptyStrategy() $this->assertEquals('-', $adm->getStrategy()); } + + public function testThrowsExceptionWhenMultipleAttributesNotAllowed() + { + $accessDecisionManager = new AccessDecisionManager(); + $traceableAccessDecisionManager = new TraceableAccessDecisionManager($accessDecisionManager); + /** @var TokenInterface&MockObject $tokenMock */ + $tokenMock = $this->createMock(TokenInterface::class); + + $this->expectException(InvalidArgumentException::class); + $traceableAccessDecisionManager->decide($tokenMock, ['attr1', 'attr2']); + } + + /** + * @dataProvider allowMultipleAttributesProvider + */ + public function testAllowMultipleAttributes(array $attributes, bool $allowMultipleAttributes) + { + $accessDecisionManager = new AccessDecisionManager(); + $traceableAccessDecisionManager = new TraceableAccessDecisionManager($accessDecisionManager); + /** @var TokenInterface&MockObject $tokenMock */ + $tokenMock = $this->createMock(TokenInterface::class); + + $isGranted = $traceableAccessDecisionManager->decide($tokenMock, $attributes, null, null, $allowMultipleAttributes); + + $this->assertFalse($isGranted); + } + + public static function allowMultipleAttributesProvider(): \Generator + { + yield [ + ['attr1'], + false, + ]; + + yield [ + ['attr1'], + true, + ]; + + yield [ + ['attr1', 'attr2', 'attr3'], + true, + ]; + } } diff --git a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php index 3902b56134eb5..e267730819571 100644 --- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php @@ -72,6 +72,10 @@ public function encode(mixed $data, string $format, array $context = []): string } elseif (!$data) { $data = [[]]; } else { + if ($data instanceof \Traversable) { + // Generators can only be iterated once — convert to array to allow multiple traversals + $data = iterator_to_array($data); + } // Sequential arrays of arrays are considered as collections $i = 0; foreach ($data as $key => $value) { diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php index 048d790b04f69..34cc940a7d0b3 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php @@ -708,6 +708,30 @@ public function testEndOfLinePassedInConstructor() $this->assertSame("foo,bar\r\nhello,test\r\n", $encoder->encode($value, 'csv')); } + /** @dataProvider provideIterable */ + public function testIterable(mixed $data) + { + $this->assertEquals(<<<'CSV' + foo,bar + hello,"hey ho" + hi,"let's go" + + CSV, $this->encoder->encode($data, 'csv')); + } + + public static function provideIterable() + { + $data = [ + ['foo' => 'hello', 'bar' => 'hey ho'], + ['foo' => 'hi', 'bar' => 'let\'s go'], + ]; + + yield 'array' => [$data]; + yield 'array iterator' => [new \ArrayIterator($data)]; + yield 'iterator aggregate' => [new \IteratorIterator(new \ArrayIterator($data))]; + yield 'generator' => [(fn (): \Generator => yield from $data)()]; + } + /** * @group legacy */ diff --git a/src/Symfony/Component/Uid/Exception/InvalidUlidException.php b/src/Symfony/Component/Uid/Exception/InvalidUlidException.php deleted file mode 100644 index cfb42ac5867a7..0000000000000 --- a/src/Symfony/Component/Uid/Exception/InvalidUlidException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Uid\Exception; - -class InvalidUlidException extends InvalidArgumentException -{ - public function __construct(string $value) - { - parent::__construct(\sprintf('Invalid ULID: "%s".', $value)); - } -} diff --git a/src/Symfony/Component/Uid/Exception/InvalidUuidException.php b/src/Symfony/Component/Uid/Exception/InvalidUuidException.php deleted file mode 100644 index 97009412b9c63..0000000000000 --- a/src/Symfony/Component/Uid/Exception/InvalidUuidException.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Uid\Exception; - -class InvalidUuidException extends InvalidArgumentException -{ - public function __construct( - public readonly int $type, - string $value, - ) { - parent::__construct(\sprintf('Invalid UUID%s: "%s".', $type ? 'v'.$type : '', $value)); - } -} diff --git a/src/Symfony/Component/Uid/Tests/UlidTest.php b/src/Symfony/Component/Uid/Tests/UlidTest.php index fe1e15b4cedde..f34660fbfd393 100644 --- a/src/Symfony/Component/Uid/Tests/UlidTest.php +++ b/src/Symfony/Component/Uid/Tests/UlidTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Uid\Exception\InvalidArgumentException; -use Symfony\Component\Uid\Exception\InvalidUlidException; use Symfony\Component\Uid\MaxUlid; use Symfony\Component\Uid\NilUlid; use Symfony\Component\Uid\Tests\Fixtures\CustomUlid; @@ -43,7 +42,7 @@ public function testGenerate() public function testWithInvalidUlid() { - $this->expectException(InvalidUlidException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid ULID: "this is not a ulid".'); new Ulid('this is not a ulid'); diff --git a/src/Symfony/Component/Uid/Ulid.php b/src/Symfony/Component/Uid/Ulid.php index 9170d429b0eb7..5ea3b84051880 100644 --- a/src/Symfony/Component/Uid/Ulid.php +++ b/src/Symfony/Component/Uid/Ulid.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Uid; use Symfony\Component\Uid\Exception\InvalidArgumentException; -use Symfony\Component\Uid\Exception\InvalidUlidException; /** * A ULID is lexicographically sortable and contains a 48-bit timestamp and 80-bit of crypto-random entropy. @@ -39,7 +38,7 @@ public function __construct(?string $ulid = null) $this->uid = $ulid; } else { if (!self::isValid($ulid)) { - throw new InvalidUlidException($ulid); + throw new InvalidArgumentException(\sprintf('Invalid ULID: "%s".', $ulid)); } $this->uid = strtoupper($ulid); diff --git a/src/Symfony/Component/Uid/Uuid.php b/src/Symfony/Component/Uid/Uuid.php index 66717f2ca1d2e..e1c9735ee85fe 100644 --- a/src/Symfony/Component/Uid/Uuid.php +++ b/src/Symfony/Component/Uid/Uuid.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Uid; -use Symfony\Component\Uid\Exception\InvalidUuidException; +use Symfony\Component\Uid\Exception\InvalidArgumentException; /** * @author Grégoire Pineau @@ -41,13 +41,13 @@ public function __construct(string $uuid, bool $checkVariant = false) $type = preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $uuid) ? (int) $uuid[14] : false; if (false === $type || (static::TYPE ?: $type) !== $type) { - throw new InvalidUuidException(static::TYPE, $uuid); + throw new InvalidArgumentException(\sprintf('Invalid UUID%s: "%s".', static::TYPE ? 'v'.static::TYPE : '', $uuid)); } $this->uid = strtolower($uuid); if ($checkVariant && !\in_array($this->uid[19], ['8', '9', 'a', 'b'], true)) { - throw new InvalidUuidException(static::TYPE, $uuid); + throw new InvalidArgumentException(\sprintf('Invalid UUID%s: "%s".', static::TYPE ? 'v'.static::TYPE : '', $uuid)); } } diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index ae1ae20da804d..a7363d7f59c19 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -8,7 +8,6 @@ CHANGELOG * Deprecate defining custom constraints not supporting named arguments * Deprecate passing an array of options to the constructors of the constraint classes, pass each option as a dedicated argument instead * Add support for ratio checks for SVG files to the `Image` constraint - * Add the `Slug` constraint * Add support for the `otherwise` option in the `When` constraint * Add support for multiple fields containing nested constraints in `Composite` constraints * Add the `stopOnFirstError` option to the `Unique` constraint to validate all elements diff --git a/src/Symfony/Component/Validator/Constraints/Image.php b/src/Symfony/Component/Validator/Constraints/Image.php index 59cd6ba883a32..5a4b3e12960e8 100644 --- a/src/Symfony/Component/Validator/Constraints/Image.php +++ b/src/Symfony/Component/Validator/Constraints/Image.php @@ -58,7 +58,7 @@ class Image extends File self::CORRUPTED_IMAGE_ERROR => 'CORRUPTED_IMAGE_ERROR', ]; - public array|string $mimeTypes = 'image/*'; + public array|string $mimeTypes = []; public ?int $minWidth = null; public ?int $maxWidth = null; public ?int $maxHeight = null; @@ -165,6 +165,8 @@ public function __construct( ?string $corruptedMessage = null, ?array $groups = null, mixed $payload = null, + array|string|null $extensions = null, + ?string $extensionsMessage = null, ?string $filenameCharset = null, ?string $filenameCountUnit = null, ?string $filenameCharsetMessage = null, @@ -191,6 +193,8 @@ public function __construct( $uploadErrorMessage, $groups, $payload, + $extensions, + $extensionsMessage, $filenameCharset, $filenameCountUnit, $filenameCharsetMessage, @@ -222,6 +226,10 @@ public function __construct( $this->allowPortraitMessage = $allowPortraitMessage ?? $this->allowPortraitMessage; $this->corruptedMessage = $corruptedMessage ?? $this->corruptedMessage; + if ([] === $this->mimeTypes && [] === $this->extensions) { + $this->mimeTypes = 'image/*'; + } + if (!\in_array('image/*', (array) $this->mimeTypes, true) && !\array_key_exists('mimeTypesMessage', $options ?? []) && null === $mimeTypesMessage) { $this->mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.'; } diff --git a/src/Symfony/Component/Validator/Constraints/Slug.php b/src/Symfony/Component/Validator/Constraints/Slug.php deleted file mode 100644 index 52a5d94c2d93b..0000000000000 --- a/src/Symfony/Component/Validator/Constraints/Slug.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Constraints; - -use Symfony\Component\Validator\Attribute\HasNamedArguments; -use Symfony\Component\Validator\Constraint; - -/** - * Validates that a value is a valid slug. - * - * @author Raffaele Carelle - */ -#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class Slug extends Constraint -{ - public const NOT_SLUG_ERROR = '14e6df1e-c8ab-4395-b6ce-04b132a3765e'; - - public string $message = 'This value is not a valid slug.'; - public string $regex = '/^[a-z0-9]+(?:-[a-z0-9]+)*$/'; - - #[HasNamedArguments] - public function __construct( - ?string $regex = null, - ?string $message = null, - ?array $groups = null, - mixed $payload = null, - ) { - parent::__construct([], $groups, $payload); - - $this->message = $message ?? $this->message; - $this->regex = $regex ?? $this->regex; - } -} diff --git a/src/Symfony/Component/Validator/Constraints/SlugValidator.php b/src/Symfony/Component/Validator/Constraints/SlugValidator.php deleted file mode 100644 index b914cad31b466..0000000000000 --- a/src/Symfony/Component/Validator/Constraints/SlugValidator.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Constraints; - -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\ConstraintValidator; -use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Exception\UnexpectedValueException; - -/** - * @author Raffaele Carelle - */ -class SlugValidator extends ConstraintValidator -{ - public function validate(mixed $value, Constraint $constraint): void - { - if (!$constraint instanceof Slug) { - throw new UnexpectedTypeException($constraint, Slug::class); - } - - if (null === $value || '' === $value) { - return; - } - - if (!\is_scalar($value) && !$value instanceof \Stringable) { - throw new UnexpectedValueException($value, 'string'); - } - - $value = (string) $value; - - if (0 === preg_match($constraint->regex, $value)) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) - ->setCode(Slug::NOT_SLUG_ERROR) - ->addViolation(); - } - } -} diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.af.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.af.xlf index de23860799dc6..9f53b1afe35c3 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.af.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.af.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Hierdie waarde mag nie na week "{{ max }}" kom nie. - - This value is not a valid slug. - Hierdie waarde is nie 'n geldige slug nie. - This value is not a valid Twig template. - This value is not a valid Twig template. + Hierdie waarde is nie 'n geldige Twig-sjabloon nie. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf index f1792bb427644..827eed1bcc86e 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". يجب ألا تكون هذه القيمة بعد الأسبوع "{{ max }}". - - This value is not a valid slug. - هذه القيمة ليست رمزا صالحا. - This value is not a valid Twig template. - This value is not a valid Twig template. + هذه القيمة ليست نموذج Twig صالح. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.az.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.az.xlf index ab15702b6f30a..9332c9ec2fd93 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.az.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.az.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Bu dəyər "{{ max }}" həftəsindən sonra olmamalıdır. - - This value is not a valid slug. - Bu dəyər etibarlı slug deyil. - This value is not a valid Twig template. - This value is not a valid Twig template. + Bu dəyər etibarlı Twig şablonu deyil. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.be.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.be.xlf index 5f4448d1b433e..7b24df5e5c189 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.be.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.be.xlf @@ -136,7 +136,7 @@ This value is not a valid IP address. - Гэта значэнне не з'яўляецца сапраўдным IP-адрасам. + Гэта значэнне не з'яўляецца сапраўдным IP-адрасам. This value is not a valid language. @@ -224,7 +224,7 @@ This value is not a valid International Bank Account Number (IBAN). - Гэта значэнне не з'яўляецца сапраўдным міжнародным нумарам банкаўскага рахунку (IBAN). + Гэта значэнне не з'яўляецца сапраўдным міжнародным нумарам банкаўскага рахунку (IBAN). This value is not a valid ISBN-10. @@ -312,7 +312,7 @@ This value is not a valid Business Identifier Code (BIC). - Гэта значэнне не з'яўляецца сапраўдным кодам ідэнтыфікацыі бізнесу (BIC). + Гэта значэнне не з'яўляецца сапраўдным кодам ідэнтыфікацыі банка (BIC). Error @@ -320,7 +320,7 @@ This value is not a valid UUID. - Гэта значэнне не з'яўляецца сапраўдным UUID. + Гэта значэнне не з'яўляецца сапраўдным UUID. This value should be a multiple of {{ compared_value }}. @@ -428,51 +428,47 @@ The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}. - Пашырэнне файла няслушнае ({{ extension }}). Дазволеныя пашырэнні: {{ extensions }}. + Пашырэнне файла недапушчальнае ({{ extension }}). Дазволеныя пашырэнні: {{ extensions }}. The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}. - Выяўленая кадыроўка знакаў няслушная ({{ detected }}). Дазволеныя кадыроўкі: {{ encodings }}. + Выяўленая кадзіроўка недапушчальная ({{ detected }}). Дазволеныя кадзіроўкі: {{ encodings }}. This value is not a valid MAC address. - Гэта значэнне не з'яўляецца сапраўдным MAC-адрасам. + Гэта значэнне не з'яўляецца сапраўдным MAC-адрасам. This URL is missing a top-level domain. - Гэтаму URL бракуе дамен верхняга ўзроўню. + Гэтаму URL бракуе дамен верхняга ўзроўню. This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. - Гэта значэнне занадта кароткае. Яно павінна ўтрымліваць хаця б адно слова.|Гэта значэнне занадта кароткае. Яно павінна ўтрымліваць хаця б {{ min }} словы. + Гэта значэнне занадта кароткае. Яно павінна ўтрымліваць хаця б адно слова.|Гэта значэнне занадта кароткае. Яно павінна ўтрымліваць хаця б {{ min }} слоў. This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. - Гэта значэнне занадта доўгае. Яно павінна ўтрымліваць адно слова.|Гэта значэнне занадта доўгае. Яно павінна ўтрымліваць {{ max }} словы або менш. + Гэта значэнне занадта доўгае. Яно павінна ўтрымліваць адно слова.|Гэта значэнне занадта доўгае. Яно павінна ўтрымліваць {{ max }} слоў або менш. This value does not represent a valid week in the ISO 8601 format. - Гэта значэнне не адпавядае правільнаму тыдні ў фармаце ISO 8601. + Гэта значэнне не адпавядае сапраўднаму тыдню ў фармаце ISO 8601. This value is not a valid week. - Гэта значэнне не з'яўляецца сапраўдным тыднем. + Гэта значэнне не з'яўляецца сапраўдным тыднем. This value should not be before week "{{ min }}". - Гэта значэнне не павінна быць раней за тыдзень "{{ min }}". + Гэта значэнне не павінна быць раней за тыдзень "{{ min }}". This value should not be after week "{{ max }}". - Гэта значэнне не павінна быць пасля тыдня "{{ max }}". - - - This value is not a valid slug. - Гэта значэнне не з'яўляецца сапраўдным слугам. + Гэта значэнне не павінна быць пасля тыдня "{{ max }}". This value is not a valid Twig template. - This value is not a valid Twig template. + Гэта значэнне не з'яўляецца сапраўдным шаблонам Twig. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.bg.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.bg.xlf index 333187eef9826..afd7590f51cb9 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.bg.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.bg.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Тази стойност не трябва да бъде след седмица "{{ max }}". - - This value is not a valid slug. - Тази стойност не е валиден слаг. - This value is not a valid Twig template. - This value is not a valid Twig template. + Тази стойност не е валиден Twig шаблон. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.bs.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.bs.xlf index e27274a36f216..d6b7de5768ee5 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.bs.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.bs.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Ova vrijednost ne bi trebala biti nakon sedmice "{{ max }}". - - This value is not a valid slug. - Ova vrijednost nije važeći slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Ova vrijednost nije važeći Twig šablon. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ca.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ca.xlf index 5506b4672974b..d656ef540f825 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ca.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ca.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Aquest valor no ha de ser posterior a la setmana "{{ max }}". - - This value is not a valid slug. - Aquest valor no és un slug vàlid. - This value is not a valid Twig template. - This value is not a valid Twig template. + Aquest valor no és una plantilla Twig vàlida. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf index 87a03badd9f15..2a2e559b95238 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Tato hodnota by neměla být týden za "{{ max }}". - - This value is not a valid slug. - Tato hodnota není platný slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Tato hodnota není platná šablona Twig. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.cy.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.cy.xlf index 98e481b67b70d..08a76667d5f4a 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.cy.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.cy.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Ni ddylai'r gwerth hwn fod ar ôl yr wythnos "{{ max }}". - - This value is not a valid slug. - Nid yw'r gwerth hwn yn slug dilys. - This value is not a valid Twig template. - This value is not a valid Twig template. + Nid yw'r gwerth hwn yn dempled Twig dilys. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.da.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.da.xlf index 976ee850e4325..bb05bba2e641c 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.da.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.da.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Denne værdi bør ikke være efter uge "{{ max }}". - - This value is not a valid slug. - Denne værdi er ikke en gyldig slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Denne værdi er ikke en gyldig Twig-skabelon. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf index 7320d3de53dfe..f02c56c6c5ca9 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf @@ -466,10 +466,6 @@ This value should not be after week "{{ max }}". Dieser Wert darf nicht nach der Woche "{{ max }}" sein. - - This value is not a valid slug. - Dieser Wert ist kein gültiger Slug. - This value is not a valid Twig template. Dieser Wert ist kein valides Twig-Template. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf index fe490aacddb40..9aec12ff82ce7 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Αυτή η τιμή δεν πρέπει να είναι μετά την εβδομάδα "{{ max }}". - - This value is not a valid slug. - Αυτή η τιμή δεν είναι έγκυρο slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Αυτή η τιμή δεν είναι έγκυρο πρότυπο Twig. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf index cad8466103fa8..f8c664f18c423 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf @@ -466,10 +466,6 @@ This value should not be after week "{{ max }}". This value should not be after week "{{ max }}". - - This value is not a valid slug. - This value is not a valid slug. - This value is not a valid Twig template. This value is not a valid Twig template. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf index 2bd3433990ded..a9ad8a76b11e9 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Este valor no debe ser posterior a la semana "{{ max }}". - - This value is not a valid slug. - Este valor no es un slug válido. - This value is not a valid Twig template. - This value is not a valid Twig template. + Este valor no es una plantilla Twig válida. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.et.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.et.xlf index 1317d41955a47..2375aa4ade30c 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.et.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.et.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". See väärtus ei tohiks olla pärast nädalat "{{ max }}". - - This value is not a valid slug. - See väärtus ei ole kehtiv slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + See väärtus ei ole kehtiv Twig'i mall. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.eu.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.eu.xlf index f92ab9638581f..830f8673dff94 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.eu.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.eu.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Balio hau ez luke astearen "{{ max }}" ondoren egon behar. - - This value is not a valid slug. - Balio hau ez da slug balioduna. - This value is not a valid Twig template. - This value is not a valid Twig template. + Balio hau ez da Twig txantiloi baliozko bat. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf index 73a97fb3cae28..f47633fd4a624 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". این مقدار نباید بعد از هفته "{{ max }}" باشد. - - This value is not a valid slug. - این مقدار یک slug معتبر نیست. - This value is not a valid Twig template. - This value is not a valid Twig template. + این مقدار یک قالب معتبر Twig نیست. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fi.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fi.xlf index 044beb1c44cc4..c046963f7ec66 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fi.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fi.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Tämän arvon ei pitäisi olla viikon "{{ max }}" jälkeen. - - This value is not a valid slug. - Tämä arvo ei ole kelvollinen slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Tämä arvo ei ole kelvollinen Twig-malli. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf index 07953955b92d5..13033c01973c8 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Cette valeur ne doit pas être postérieure à la semaine "{{ max }}". - - This value is not a valid slug. - Cette valeur n'est pas un slug valide. - This value is not a valid Twig template. - This value is not a valid Twig template. + Cette valeur n'est pas un modèle Twig valide. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.gl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.gl.xlf index c85e942f36040..391d741e96564 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.gl.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.gl.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Este valor non debe estar despois da semana "{{ max }}". - - This value is not a valid slug. - Este valor non é un slug válido. - This value is not a valid Twig template. - This value is not a valid Twig template. + Este valor non é un modelo Twig válido. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.he.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.he.xlf index 8a985737485b4..671a98881536e 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.he.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.he.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". ערך זה לא אמור להיות לאחר שבוע "{{ max }}". - - This value is not a valid slug. - ערך זה אינו slug חוקי. - This value is not a valid Twig template. - This value is not a valid Twig template. + ערך זה אינו תבנית Twig חוקית. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.hr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.hr.xlf index 10985f3df18a8..0951d41926514 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.hr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.hr.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Ova vrijednost ne bi trebala biti nakon tjedna "{{ max }}". - - This value is not a valid slug. - Ova vrijednost nije valjani slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Ova vrijednost nije valjani Twig predložak. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf index 2a3472dd94a2d..dffab0ccbf700 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Ez az érték nem lehet a "{{ max }}". hétnél későbbi. - - This value is not a valid slug. - Ez az érték nem érvényes slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Ez az érték nem érvényes Twig sablon. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.hy.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.hy.xlf index 0c3953a27dc29..856babbd5fe4e 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.hy.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.hy.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Այս արժեքը չպետք է լինի «{{ max }}» շաբաթից հետո։ - - This value is not a valid slug. - Այս արժեքը վավեր slug չէ: - This value is not a valid Twig template. - This value is not a valid Twig template. + Այս արժեքը վավեր Twig ձևանմուշ չէ: diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf index 7c8a3c8dfb808..b9796f888f924 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Nilai ini tidak boleh setelah minggu "{{ max }}". - - This value is not a valid slug. - Nilai ini bukan slug yang valid. - This value is not a valid Twig template. - This value is not a valid Twig template. + Nilai ini bukan templat Twig yang valid. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf index 5258cf7d3ec7a..34ef61ed5f8a7 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Questo valore non dovrebbe essere dopo la settimana "{{ max }}". - - This value is not a valid slug. - Questo valore non è uno slug valido. - This value is not a valid Twig template. - This value is not a valid Twig template. + Questo valore non è un template Twig valido. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf index ae0e734b45c67..e83aced818553 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". この値は週 "{{ max }}" 以降であってはいけません。 - - This value is not a valid slug. - この値は有効なスラグではありません。 - This value is not a valid Twig template. - This value is not a valid Twig template. + この値は有効な Twig テンプレートではありません。 diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.lb.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.lb.xlf index 2961aec0b0ef2..d1b5cef57bd0e 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.lb.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.lb.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Dëse Wäert sollt net no Woch "{{ max }}" sinn. - - This value is not a valid slug. - Dëse Wäert ass kee gültege Slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Dëse Wäert ass kee valabelen Twig-Template. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf index 9c56c8377cdd8..46abd95036044 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Ši reikšmė neturėtų būti po savaitės "{{ max }}". - - This value is not a valid slug. - Ši reikšmė nėra tinkamas slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Ši reikšmė nėra tinkamas „Twig“ šablonas. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.lv.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.lv.xlf index db61de4f486d5..3e2d51a30dec1 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.lv.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.lv.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Šai vērtībai nevajadzētu būt pēc "{{ max }}" nedēļas. - - This value is not a valid slug. - Šī vērtība nav derīgs slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Šī vērtība nav derīgs Twig šablons. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.mk.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.mk.xlf index 7d9a63dbd7e36..99b1a191b6c0d 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.mk.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.mk.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Ова вредност не треба да биде по недела "{{ max }}". - - This value is not a valid slug. - Оваа вредност не е валиден slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Оваа вредност не е валиден Twig шаблон. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.mn.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.mn.xlf index 222526fe24b19..3344675d9ae6a 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.mn.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.mn.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Энэ утга нь долоо хоног "{{ max }}" -аас хойш байх ёсгүй. - - This value is not a valid slug. - Энэ утга хүчинтэй slug биш байна. - This value is not a valid Twig template. - This value is not a valid Twig template. + Энэ утга нь Twig-ийн хүчинтэй загвар биш юм. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.my.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.my.xlf index 90b6648acc594..04c955f754509 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.my.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.my.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". ဤတန်ဖိုးသည် သီတင်းပတ် "{{ max }}" ပြီးနောက် ဖြစ်သင့်သည်မဟုတ်ပါ။ - - This value is not a valid slug. - ဒီတန်ဖိုးသည်မှန်ကန်သော slug မဟုတ်ပါ။ - This value is not a valid Twig template. - This value is not a valid Twig template. + ဤတန်ဖိုးသည် မှန်ကန်သော Twig တင်းပလိတ်မဟုတ်ပါ။ diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.nb.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.nb.xlf index 0997c1af56a14..58696f671ca4a 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.nb.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.nb.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Denne verdien bør ikke være etter uke "{{ max }}". - - This value is not a valid slug. - Denne verdien er ikke en gyldig slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Denne verdien er ikke en gyldig Twig-mal. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf index 820bb69aae42e..0e0de772720c4 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Deze waarde mag niet na week "{{ max }}" liggen. - - This value is not a valid slug. - Deze waarde is geen geldige slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Deze waarde is geen geldige Twig-template. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.nn.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.nn.xlf index 6a36a1cc2571f..74d332c06efb2 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.nn.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.nn.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Denne verdien bør ikkje vere etter veke "{{ max }}". - - This value is not a valid slug. - Denne verdien er ikkje ein gyldig slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Denne verdien er ikkje ein gyldig Twig-mal. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.no.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.no.xlf index 0997c1af56a14..58696f671ca4a 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.no.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.no.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Denne verdien bør ikke være etter uke "{{ max }}". - - This value is not a valid slug. - Denne verdien er ikke en gyldig slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Denne verdien er ikke en gyldig Twig-mal. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.pl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.pl.xlf index 40a3212bc073c..7c243a6b0ca02 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.pl.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.pl.xlf @@ -466,10 +466,6 @@ This value should not be after week "{{ max }}". Ta wartość nie powinna być po tygodniu "{{ max }}". - - This value is not a valid slug. - Ta wartość nie jest prawidłowym slugiem. - This value is not a valid Twig template. Ta wartość nie jest prawidłowym szablonem Twig. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf index 0d685d524cd69..b6562dbfe712f 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Este valor não deve estar após a semana "{{ max }}". - - This value is not a valid slug. - Este valor não é um slug válido. - This value is not a valid Twig template. - This value is not a valid Twig template. + Este valor não é um modelo Twig válido. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf index 3dbdd4ea2d675..a6be16580c6bd 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Este valor não deve estar após a semana "{{ max }}". - - This value is not a valid slug. - Este valor não é um slug válido. - This value is not a valid Twig template. - This value is not a valid Twig template. + Este valor não é um modelo Twig válido. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf index bed709ceaf927..d6c3b4fb82984 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Această valoare nu trebuie să fie după săptămâna "{{ max }}". - - This value is not a valid slug. - Această valoare nu este un slug valid. - This value is not a valid Twig template. - This value is not a valid Twig template. + Această valoare nu este un șablon Twig valid. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ru.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ru.xlf index 5fc8d14d833ae..727ae0aefdf86 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ru.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ru.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Это значение не должно быть после недели "{{ max }}". - - This value is not a valid slug. - Это значение не является допустимым slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Это значение не является корректным шаблоном Twig. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.sk.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.sk.xlf index 253b4cd8c37d1..bba3c291a7fed 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.sk.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.sk.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Táto hodnota by nemala byť po týždni "{{ max }}". - - This value is not a valid slug. - Táto hodnota nie je platný slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Táto hodnota nie je platná šablóna Twig. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.sl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.sl.xlf index 669d8e85d8a5c..28c370e096887 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.sl.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.sl.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Ta vrednost ne sme biti po tednu "{{ max }}". - - This value is not a valid slug. - Ta vrednost ni veljaven URL slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Ta vrednost ni veljavna predloga Twig. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.sq.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.sq.xlf index 1933143261af8..ba7178f672ef7 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.sq.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.sq.xlf @@ -475,13 +475,9 @@ This value should not be after week "{{ max }}". Kjo vlerë nuk duhet të jetë pas javës "{{ max }}". - - This value is not a valid slug. - Kjo vlerë nuk është një slug i vlefshëm. - This value is not a valid Twig template. - This value is not a valid Twig template. + Kjo vlerë nuk është një shabllon Twig i vlefshëm. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.sr_Cyrl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.sr_Cyrl.xlf index d36e83ca62785..61040270ac884 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.sr_Cyrl.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.sr_Cyrl.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Ова вредност не треба да буде после недеље "{{ max }}". - - This value is not a valid slug. - Ова вредност није валидан слуг. - This value is not a valid Twig template. - This value is not a valid Twig template. + Ова вредност није важећи Twig шаблон. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.sr_Latn.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.sr_Latn.xlf index ba9092a1c0c4c..be7ede71376d7 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.sr_Latn.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.sr_Latn.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Ova vrednost ne treba da bude posle nedelje "{{ max }}". - - This value is not a valid slug. - Ova vrednost nije validan slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Ova vrednost nije važeći Twig šablon. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.sv.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.sv.xlf index 8ed255071e270..692ac7f52d243 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.sv.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.sv.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Det här värdet bör inte vara efter vecka "{{ max }}". - - This value is not a valid slug. - Detta värde är inte en giltig slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Det här värdet är inte en giltig Twig-mall. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.th.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.th.xlf index de5008674af45..75398a0b84206 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.th.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.th.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". ค่านี้ไม่ควรจะอยู่หลังสัปดาห์ "{{ max }}" - - This value is not a valid slug. - ค่านี้ไม่ใช่ slug ที่ถูกต้อง - This value is not a valid Twig template. - This value is not a valid Twig template. + ค่านี้ไม่ใช่เทมเพลต Twig ที่ถูกต้อง diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.tl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.tl.xlf index 310a7a20563ae..729ebc9da9185 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.tl.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.tl.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Ang halagang ito ay hindi dapat pagkatapos ng linggo "{{ max }}". - - This value is not a valid slug. - Ang halagang ito ay hindi isang wastong slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Ang halagang ito ay hindi isang balidong Twig template. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf index 0bf57d80b7ca4..42c4bbd91ebab 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Bu değer “{{ max }}” haftasından sonra olmamalıdır - - This value is not a valid slug. - Bu değer geçerli bir “slug” değildir. - This value is not a valid Twig template. - This value is not a valid Twig template. + Bu değer geçerli bir Twig şablonu olarak kabul edilmiyor. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.uk.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.uk.xlf index 2110eb48e7dfe..5f132bc77a6ec 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.uk.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.uk.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Це значення не повинно бути після тижня "{{ max }}". - - This value is not a valid slug. - Це значення не є дійсним slug. - This value is not a valid Twig template. - This value is not a valid Twig template. + Це значення не є дійсним шаблоном Twig. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ur.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ur.xlf index 280b1488d2cf8..f13aafb43264b 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ur.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ur.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". یہ قدر ہفتہ "{{ max }}" کے بعد نہیں ہونا چاہیے۔ - - This value is not a valid slug. - یہ قدر درست سلاگ نہیں ہے۔ - This value is not a valid Twig template. - This value is not a valid Twig template. + یہ قدر ایک درست Twig سانچہ نہیں ہے۔ diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.uz.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.uz.xlf index da07805f689c2..fe0b49f715b12 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.uz.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.uz.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Bu qiymat "{{ max }}" haftadan keyin bo'lmasligi kerak. - - This value is not a valid slug. - Bu qiymat yaroqli slug emas. - This value is not a valid Twig template. - This value is not a valid Twig template. + Bu qiymat yaroqli Twig shabloni emas. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.vi.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.vi.xlf index 048be7f2c4336..9daa4fe8a29d5 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.vi.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.vi.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". Giá trị này không nên sau tuần "{{ max }}". - - This value is not a valid slug. - Giá trị này không phải là một slug hợp lệ. - This value is not a valid Twig template. - This value is not a valid Twig template. + Giá trị này không phải là một mẫu Twig hợp lệ. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.zh_CN.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.zh_CN.xlf index 24a89c15b8b91..c570d936ff17c 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.zh_CN.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.zh_CN.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". 该值不应位于 "{{ max }}"周之后。 - - This value is not a valid slug. - 此值不是有效的 slug。 - This value is not a valid Twig template. - This value is not a valid Twig template. + 此值不是有效的 Twig 模板。 diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.zh_TW.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.zh_TW.xlf index 783461efa4c7f..a60283b280898 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.zh_TW.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.zh_TW.xlf @@ -466,13 +466,9 @@ This value should not be after week "{{ max }}". 這個數值不應晚於第「{{ max }}」週。 - - This value is not a valid slug. - 這個數值不是有效的 slug。 - This value is not a valid Twig template. - This value is not a valid Twig template. + 這個數值不是有效的 Twig 模板。 diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php index a79bd9dd4900a..7a7aa919793ba 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Tests\Constraints; +use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Validator\Constraints\Image; use Symfony\Component\Validator\Constraints\ImageValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; @@ -688,4 +690,75 @@ public static function provideSvgWithoutViolation(): iterable new Image(maxRatio: 1, maxRatioMessage: 'myMessage'), ]; } + + /** + * @dataProvider providerValidExtension + */ + public function testExtensionValid(string $name) + { + if (!class_exists(MimeTypes::class)) { + $this->markTestSkipped('Guessing the mime type is not possible'); + } + + $constraint = new Image(mimeTypes: [], extensions: ['gif'], extensionsMessage: 'myMessage'); + + $this->validator->validate(new File(__DIR__.'/Fixtures/'.$name), $constraint); + + $this->assertNoViolation(); + } + + public static function providerValidExtension(): iterable + { + yield ['test.gif']; + yield ['test.png.gif']; + } + + /** + * @dataProvider provideInvalidExtension + */ + public function testExtensionInvalid(string $name, string $extension) + { + $path = __DIR__.'/Fixtures/'.$name; + $constraint = new Image(extensions: ['png', 'svg'], extensionsMessage: 'myMessage'); + + $this->validator->validate(new File($path), $constraint); + + $this->buildViolation('myMessage') + ->setParameters([ + '{{ file }}' => '"'.$path.'"', + '{{ extension }}' => '"'.$extension.'"', + '{{ extensions }}' => '"png", "svg"', + '{{ name }}' => '"'.$name.'"', + ]) + ->setCode(Image::INVALID_EXTENSION_ERROR) + ->assertRaised(); + } + + public static function provideInvalidExtension(): iterable + { + yield ['test.gif', 'gif']; + yield ['test.png.gif', 'gif']; + } + + public function testExtensionAutodetectMimeTypesInvalid() + { + if (!class_exists(MimeTypes::class)) { + $this->markTestSkipped('Guessing the mime type is not possible'); + } + + $path = __DIR__.'/Fixtures/invalid-content.gif'; + $constraint = new Image(mimeTypesMessage: 'myMessage', extensions: ['gif']); + + $this->validator->validate(new File($path), $constraint); + + $this->buildViolation('myMessage') + ->setParameters([ + '{{ file }}' => '"'.$path.'"', + '{{ name }}' => '"invalid-content.gif"', + '{{ type }}' => '"text/plain"', + '{{ types }}' => '"image/gif"', + ]) + ->setCode(Image::INVALID_MIME_TYPE_ERROR) + ->assertRaised(); + } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SlugTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SlugTest.php deleted file mode 100644 index a2c5b07d3f873..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Constraints/SlugTest.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Constraints; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\Validator\Constraints\Slug; -use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; - -class SlugTest extends TestCase -{ - public function testAttributes() - { - $metadata = new ClassMetadata(SlugDummy::class); - $loader = new AttributeLoader(); - self::assertTrue($loader->loadClassMetadata($metadata)); - - [$bConstraint] = $metadata->properties['b']->getConstraints(); - self::assertSame('myMessage', $bConstraint->message); - self::assertSame(['Default', 'SlugDummy'], $bConstraint->groups); - - [$cConstraint] = $metadata->properties['c']->getConstraints(); - self::assertSame(['my_group'], $cConstraint->groups); - self::assertSame('some attached data', $cConstraint->payload); - } -} - -class SlugDummy -{ - #[Slug] - private $a; - - #[Slug(message: 'myMessage')] - private $b; - - #[Slug(groups: ['my_group'], payload: 'some attached data')] - private $c; -} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SlugValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SlugValidatorTest.php deleted file mode 100644 index e8d210b8377e3..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Constraints/SlugValidatorTest.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Constraints; - -use Symfony\Component\Validator\Constraints\Slug; -use Symfony\Component\Validator\Constraints\SlugValidator; -use Symfony\Component\Validator\Exception\UnexpectedValueException; -use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; - -class SlugValidatorTest extends ConstraintValidatorTestCase -{ - protected function createValidator(): SlugValidator - { - return new SlugValidator(); - } - - public function testNullIsValid() - { - $this->validator->validate(null, new Slug()); - - $this->assertNoViolation(); - } - - public function testEmptyStringIsValid() - { - $this->validator->validate('', new Slug()); - - $this->assertNoViolation(); - } - - public function testExpectsStringCompatibleType() - { - $this->expectException(UnexpectedValueException::class); - $this->validator->validate(new \stdClass(), new Slug()); - } - - /** - * @testWith ["test-slug"] - * ["slug-123-test"] - * ["slug"] - */ - public function testValidSlugs($slug) - { - $this->validator->validate($slug, new Slug()); - - $this->assertNoViolation(); - } - - /** - * @testWith ["NotASlug"] - * ["Not a slug"] - * ["not-á-slug"] - * ["not-@-slug"] - */ - public function testInvalidSlugs($slug) - { - $constraint = new Slug(message: 'myMessage'); - - $this->validator->validate($slug, $constraint); - - $this->buildViolation('myMessage') - ->setParameter('{{ value }}', '"'.$slug.'"') - ->setCode(Slug::NOT_SLUG_ERROR) - ->assertRaised(); - } - - /** - * @testWith ["test-slug", true] - * ["slug-123-test", true] - */ - public function testCustomRegexInvalidSlugs($slug) - { - $constraint = new Slug(regex: '/^[a-z0-9]+$/i'); - - $this->validator->validate($slug, $constraint); - - $this->buildViolation($constraint->message) - ->setParameter('{{ value }}', '"'.$slug.'"') - ->setCode(Slug::NOT_SLUG_ERROR) - ->assertRaised(); - } - - /** - * @testWith ["slug"] - * @testWith ["test1234"] - */ - public function testCustomRegexValidSlugs($slug) - { - $constraint = new Slug(regex: '/^[a-z0-9]+$/i'); - - $this->validator->validate($slug, $constraint); - - $this->assertNoViolation(); - } -} diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 5177d37d2955a..0eaac5f6bf735 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -38,6 +38,7 @@ "symfony/mime": "^6.4|^7.0", "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", "symfony/translation": "^6.4.3|^7.0.3", "symfony/type-info": "^7.1", "egulias/email-validator": "^2.1.10|^3|^4" diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index 5afa5529b0d3d..b815e7040c501 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -86,7 +86,7 @@ public static function generateLazyGhost(\ReflectionClass $class): string .($p->isProtected() ? 'protected' : 'public') .($p->isProtectedSet() ? ' protected(set)' : '') ." {$type} \${$name}" - .($p->hasDefaultValue() ? ' = '.$p->getDefaultValue() : '') + .($p->hasDefaultValue() ? ' = '.VarExporter::export($p->getDefaultValue()) : '') ." {\n"; foreach ($p->getHooks() as $hook => $method) { diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedWithDefaultValue.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedWithDefaultValue.php index 1281109e7228d..7d49049ac449a 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedWithDefaultValue.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedWithDefaultValue.php @@ -4,8 +4,18 @@ class HookedWithDefaultValue { - public int $backedWithDefault = 321 { - get => $this->backedWithDefault; - set => $this->backedWithDefault = $value; + public int $backedIntWithDefault = 321 { + get => $this->backedIntWithDefault; + set => $this->backedIntWithDefault = $value; + } + + public string $backedStringWithDefault = '321' { + get => $this->backedStringWithDefault; + set => $this->backedStringWithDefault = $value; + } + + public bool $backedBoolWithDefault = false { + get => $this->backedBoolWithDefault; + set => $this->backedBoolWithDefault = $value; } } diff --git a/src/Symfony/Component/VarExporter/Tests/LegacyLazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LegacyLazyGhostTraitTest.php index e86fb841b70eb..c650626847055 100644 --- a/src/Symfony/Component/VarExporter/Tests/LegacyLazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LegacyLazyGhostTraitTest.php @@ -332,16 +332,22 @@ public function testPropertyHooksWithDefaultValue() $initialized = true; }); - $this->assertSame(321, $object->backedWithDefault); + $this->assertSame(321, $object->backedIntWithDefault); + $this->assertSame('321', $object->backedStringWithDefault); + $this->assertSame(false, $object->backedBoolWithDefault); $this->assertTrue($initialized); $initialized = false; $object = $this->createLazyGhost(HookedWithDefaultValue::class, function ($instance) use (&$initialized) { $initialized = true; }); - $object->backedWithDefault = 654; + $object->backedIntWithDefault = 654; + $object->backedStringWithDefault = '654'; + $object->backedBoolWithDefault = true; $this->assertTrue($initialized); - $this->assertSame(654, $object->backedWithDefault); + $this->assertSame(654, $object->backedIntWithDefault); + $this->assertSame('654', $object->backedStringWithDefault); + $this->assertSame(true, $object->backedBoolWithDefault); } /** diff --git a/src/Symfony/Component/WebLink/Link.php b/src/Symfony/Component/WebLink/Link.php index f8bf6e4f52e01..1f5fbbdf9c6b5 100644 --- a/src/Symfony/Component/WebLink/Link.php +++ b/src/Symfony/Component/WebLink/Link.php @@ -98,6 +98,12 @@ class Link implements EvolvableLinkInterface public const REL_PREDECESSOR_VERSION = 'predecessor-version'; public const REL_PREFETCH = 'prefetch'; public const REL_PRELOAD = 'preload'; + + /** + * This feature is deprecated and superseded by the Speculation Rules API. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/prerender + */ public const REL_PRERENDER = 'prerender'; public const REL_PREV = 'prev'; public const REL_PREVIEW = 'preview';