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% хвілін.