diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 90e51d60536d6..3d21822287b6b 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,6 +1,6 @@
| Q | A
| ------------- | ---
-| Branch? | 7.3 for features / 5.4, 6.4, 7.1, and 7.2 for bug fixes
+| Branch? | 7.3 for features / 6.4, 7.1, and 7.2 for bug fixes
| Bug fix? | yes/no
| New feature? | yes/no
| Deprecations? | yes/no
diff --git a/CHANGELOG-7.2.md b/CHANGELOG-7.2.md
index 11ad8ff6b9959..681d728e832ef 100644
--- a/CHANGELOG-7.2.md
+++ b/CHANGELOG-7.2.md
@@ -7,6 +7,30 @@ in 7.2 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.2.0...v7.2.1
+* 7.2.2 (2024-12-31)
+
+ * bug #59304 [PropertyInfo] Remove ``@internal`` from `PropertyReadInfo` and `PropertyWriteInfo` (Dario Guarracino)
+ * bug #59252 [Stopwatch] bug #54854 undefined key error when trying to fetch a mis… (Alex Niedre)
+ * bug #59278 [SecurityBundle] Do not replace authenticators service by their traceable version (MatTheCat)
+ * bug #59228 [HttpFoundation] Avoid mime type guess with temp files in `BinaryFileResponse` (alexandre-daubois)
+ * bug #59318 [Finder] Fix using `==` as default operator in `DateComparator` (MatTheCat)
+ * bug #59321 [HtmlSanitizer] reject URLs containing whitespaces (xabbuh)
+ * bug #59310 [Validator] the "max" option can be zero (xabbuh)
+ * bug #59271 [TypeInfo] Fix PHPDoc resolving of union with mixed (mtarld)
+ * bug #59269 [Security/Csrf] Trust "Referer" at the same level as "Origin" (nicolas-grekas)
+ * bug #59250 [HttpClient] Fix a typo in NoPrivateNetworkHttpClient (Jontsa)
+ * bug #59103 [Messenger] ensure exception on rollback does not hide previous exception (nikophil)
+ * bug #59226 [FrameworkBundle] require the writer to implement getFormats() in the translation:extract (xabbuh)
+ * bug #59213 [FrameworkBundle] don't require fake notifier transports to be installed as non-dev dependencies (xabbuh)
+ * bug #59113 [FrameworkBundle][Translation] fix translation lint compatibility with the `PseudoLocalizationTranslator` (xabbuh)
+ * bug #59060 [Validator] set the violation path only if the `errorPath` option is set (xabbuh)
+ * bug #59066 Fix resolve enum in string type resolver (DavidBadura)
+ * bug #59156 [PropertyInfo] Fix interface handling in PhpStanTypeHelper (mtarld)
+ * bug #59160 [BeanstalkMessenger] Round delay to an integer to avoid deprecation warning (plantas)
+ * bug #59012 [PropertyInfo] Fix interface handling in `PhpStanTypeHelper` (janedbal)
+ * bug #59134 [HttpKernel] Denormalize request data using the csv format when using "#[MapQueryString]" or "#[MapRequestPayload]" (except for content data) (ovidiuenache)
+ * bug #59140 [WebProfilerBundle] fix: white-space in highlighted code (chr-hertel)
+
* 7.2.1 (2024-12-11)
* bug #59145 [TypeInfo] Make `Type::nullable` method no-op on every nullable type (mtarld)
diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php
index e4831557f01db..8e10891b0ba74 100644
--- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php
+++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php
@@ -27,15 +27,17 @@ class DoctrineTransactionMiddleware extends AbstractDoctrineMiddleware
protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope
{
$entityManager->getConnection()->beginTransaction();
+
+ $success = false;
try {
$envelope = $stack->next()->handle($envelope, $stack);
$entityManager->flush();
$entityManager->getConnection()->commit();
+ $success = true;
+
return $envelope;
} catch (\Throwable $exception) {
- $entityManager->getConnection()->rollBack();
-
if ($exception instanceof HandlerFailedException) {
// Remove all HandledStamp from the envelope so the retry will execute all handlers again.
// When a handler fails, the queries of allegedly successful previous handlers just got rolled back.
@@ -43,6 +45,12 @@ protected function handleForManager(EntityManagerInterface $entityManager, Envel
}
throw $exception;
+ } finally {
+ $connection = $entityManager->getConnection();
+
+ if (!$success && $connection->isTransactionActive()) {
+ $connection->rollBack();
+ }
}
}
}
diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php
index 977f32e30fa61..05e5dae1b34ac 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php
@@ -56,12 +56,9 @@ public function testMiddlewareWrapsInTransactionAndFlushes()
public function testTransactionIsRolledBackOnException()
{
- $this->connection->expects($this->once())
- ->method('beginTransaction')
- ;
- $this->connection->expects($this->once())
- ->method('rollBack')
- ;
+ $this->connection->expects($this->once())->method('beginTransaction');
+ $this->connection->expects($this->once())->method('isTransactionActive')->willReturn(true);
+ $this->connection->expects($this->once())->method('rollBack');
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Thrown from next middleware.');
@@ -69,6 +66,27 @@ public function testTransactionIsRolledBackOnException()
$this->middleware->handle(new Envelope(new \stdClass()), $this->getThrowingStackMock());
}
+ public function testExceptionInRollBackDoesNotHidePreviousException()
+ {
+ $this->connection->expects($this->once())->method('beginTransaction');
+ $this->connection->expects($this->once())->method('isTransactionActive')->willReturn(true);
+ $this->connection->expects($this->once())->method('rollBack')->willThrowException(new \RuntimeException('Thrown from rollBack.'));
+
+ try {
+ $this->middleware->handle(new Envelope(new \stdClass()), $this->getThrowingStackMock());
+ } catch (\Throwable $exception) {
+ }
+
+ self::assertNotNull($exception);
+ self::assertInstanceOf(\RuntimeException::class, $exception);
+ self::assertSame('Thrown from rollBack.', $exception->getMessage());
+
+ $previous = $exception->getPrevious();
+ self::assertNotNull($previous);
+ self::assertInstanceOf(\RuntimeException::class, $previous);
+ self::assertSame('Thrown from next middleware.', $previous->getMessage());
+ }
+
public function testInvalidEntityManagerThrowsException()
{
$managerRegistry = $this->createMock(ManagerRegistry::class);
diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php
index d9079b1c7ef17..ccce1de340c02 100644
--- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php
+++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php
@@ -80,7 +80,7 @@ public function testGenerateFragmentUri()
]);
$twig->addRuntimeLoader($loader);
- $this->assertSame('/_fragment?_hash=XCg0hX8QzSwik8Xuu9aMXhoCeI4oJOob7lUVacyOtyY%3D&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CController%255CTemplateController%253A%253AtemplateAction', $twig->render('index'));
+ $this->assertMatchesRegularExpression('#/_fragment\?_hash=.+&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CController%255CTemplateController%253A%253AtemplateAction$#', $twig->render('index'));
}
protected function getFragmentHandler($returnOrException): FragmentHandler
diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
index b26d3f9ad20dd..194d1c50d25d5 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
@@ -62,6 +62,10 @@ public function __construct(
private array $enabledLocales = [],
) {
parent::__construct();
+
+ if (!method_exists($writer, 'getFormats')) {
+ throw new \InvalidArgumentException(sprintf('The writer class "%s" does not implement the "getFormats()" method.', $writer::class));
+ }
}
protected function configure(): void
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationLintCommandPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationLintCommandPass.php
new file mode 100644
index 0000000000000..4756795d1beff
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationLintCommandPass.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\Translation\TranslatorBagInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+final class TranslationLintCommandPass implements CompilerPassInterface
+{
+ public function process(ContainerBuilder $container): void
+ {
+ if (!$container->hasDefinition('console.command.translation_lint') || !$container->has('translator')) {
+ return;
+ }
+
+ $translatorClass = $container->getParameterBag()->resolveValue($container->findDefinition('translator')->getClass());
+
+ if (!is_subclass_of($translatorClass, TranslatorInterface::class) || !is_subclass_of($translatorClass, TranslatorBagInterface::class)) {
+ $container->removeDefinition('console.command.translation_lint');
+ }
+ }
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php
new file mode 100644
index 0000000000000..7542191d0e83e
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+class TranslationUpdateCommandPass implements CompilerPassInterface
+{
+ public function process(ContainerBuilder $container): void
+ {
+ if (!$container->hasDefinition('console.command.translation_extract')) {
+ return;
+ }
+
+ $translationWriterClass = $container->getParameterBag()->resolveValue($container->findDefinition('translation.writer')->getClass());
+
+ if (!method_exists($translationWriterClass, 'getFormats')) {
+ $container->removeDefinition('console.command.translation_extract');
+ }
+ }
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index a6d5956033d5e..3106ee30e7b4d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -120,6 +120,8 @@
use Symfony\Component\Mime\MimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypes;
use Symfony\Component\Notifier\Bridge as NotifierBridge;
+use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory;
+use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory;
use Symfony\Component\Notifier\ChatterInterface;
use Symfony\Component\Notifier\Notifier;
use Symfony\Component\Notifier\Recipient\Recipient;
@@ -2833,8 +2835,6 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
NotifierBridge\Engagespot\EngagespotTransportFactory::class => 'notifier.transport_factory.engagespot',
NotifierBridge\Esendex\EsendexTransportFactory::class => 'notifier.transport_factory.esendex',
NotifierBridge\Expo\ExpoTransportFactory::class => 'notifier.transport_factory.expo',
- NotifierBridge\FakeChat\FakeChatTransportFactory::class => 'notifier.transport_factory.fake-chat',
- NotifierBridge\FakeSms\FakeSmsTransportFactory::class => 'notifier.transport_factory.fake-sms',
NotifierBridge\Firebase\FirebaseTransportFactory::class => 'notifier.transport_factory.firebase',
NotifierBridge\FortySixElks\FortySixElksTransportFactory::class => 'notifier.transport_factory.forty-six-elks',
NotifierBridge\FreeMobile\FreeMobileTransportFactory::class => 'notifier.transport_factory.free-mobile',
@@ -2922,20 +2922,26 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
$container->removeDefinition($classToServices[NotifierBridge\Mercure\MercureTransportFactory::class]);
}
- if (ContainerBuilder::willBeAvailable('symfony/fake-chat-notifier', NotifierBridge\FakeChat\FakeChatTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) {
- $container->getDefinition($classToServices[NotifierBridge\FakeChat\FakeChatTransportFactory::class])
- ->replaceArgument(0, new Reference('mailer'))
- ->replaceArgument(1, new Reference('logger'))
+ // don't use ContainerBuilder::willBeAvailable() as these are not needed in production
+ if (class_exists(FakeChatTransportFactory::class)) {
+ $container->getDefinition('notifier.transport_factory.fake-chat')
+ ->replaceArgument(0, new Reference('mailer', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
+ ->replaceArgument(1, new Reference('logger', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
->addArgument(new Reference('event_dispatcher', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
->addArgument(new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE));
+ } else {
+ $container->removeDefinition('notifier.transport_factory.fake-chat');
}
- if (ContainerBuilder::willBeAvailable('symfony/fake-sms-notifier', NotifierBridge\FakeSms\FakeSmsTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) {
- $container->getDefinition($classToServices[NotifierBridge\FakeSms\FakeSmsTransportFactory::class])
- ->replaceArgument(0, new Reference('mailer'))
- ->replaceArgument(1, new Reference('logger'))
+ // don't use ContainerBuilder::willBeAvailable() as these are not needed in production
+ if (class_exists(FakeSmsTransportFactory::class)) {
+ $container->getDefinition('notifier.transport_factory.fake-sms')
+ ->replaceArgument(0, new Reference('mailer', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
+ ->replaceArgument(1, new Reference('logger', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
->addArgument(new Reference('event_dispatcher', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
->addArgument(new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE));
+ } else {
+ $container->removeDefinition('notifier.transport_factory.fake-sms');
}
if (ContainerBuilder::willBeAvailable('symfony/bluesky-notifier', NotifierBridge\Bluesky\BlueskyTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier'])) {
diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
index a1eb059bb01ce..e83c4dfe611d1 100644
--- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
+++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
@@ -19,6 +19,8 @@
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass;
+use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationLintCommandPass;
+use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationUpdateCommandPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\VirtualRequestStackPass;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
@@ -149,6 +151,8 @@ public function build(ContainerBuilder $container): void
$this->addCompilerPassIfExists($container, AddConstraintValidatorsPass::class);
$this->addCompilerPassIfExists($container, AddValidatorInitializersPass::class);
$this->addCompilerPassIfExists($container, AddConsoleCommandPass::class, PassConfig::TYPE_BEFORE_REMOVING);
+ // must be registered before the AddConsoleCommandPass
+ $container->addCompilerPass(new TranslationLintCommandPass(), PassConfig::TYPE_BEFORE_REMOVING, 10);
// must be registered as late as possible to get access to all Twig paths registered in
// twig.template_iterator definition
$this->addCompilerPassIfExists($container, TranslatorPass::class, PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
@@ -181,6 +185,7 @@ public function build(ContainerBuilder $container): void
// must be registered after MonologBundle's LoggerChannelPass
$container->addCompilerPass(new ErrorLoggerCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
$container->addCompilerPass(new VirtualRequestStackPass());
+ $container->addCompilerPass(new TranslationUpdateCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
if ($container->getParameter('kernel.debug')) {
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php
index 6d8966a171ba2..48d5c327a3986 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php
@@ -50,6 +50,6 @@ public function testGenerateFragmentUri()
$client = self::createClient(['test_case' => 'Fragment', 'root_config' => 'config.yml', 'debug' => true]);
$client->request('GET', '/fragment_uri');
- $this->assertSame('/_fragment?_hash=CCRGN2D%2FoAJbeGz%2F%2FdoH3bNSPwLCrmwC1zAYCGIKJ0E%3D&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction', $client->getResponse()->getContent());
+ $this->assertMatchesRegularExpression('#/_fragment\?_hash=.+&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction$#', $client->getResponse()->getContent());
}
}
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
index 622b853d1d8c6..81c4a8ee6f46f 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
@@ -643,11 +643,13 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri
}
if ($container->hasDefinition('debug.security.firewall')) {
- foreach ($authenticationProviders as $authenticatorId) {
- $container->register('debug.'.$authenticatorId, TraceableAuthenticator::class)
- ->setDecoratedService($authenticatorId)
- ->setArguments([new Reference('debug.'.$authenticatorId.'.inner')])
+ foreach ($authenticationProviders as &$authenticatorId) {
+ $traceableId = 'debug.'.$authenticatorId;
+ $container
+ ->register($traceableId, TraceableAuthenticator::class)
+ ->setArguments([new Reference($authenticatorId)])
;
+ $authenticatorId = $traceableId;
}
}
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php
index 23aa17b9adb57..c4ae38e65b2da 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php
@@ -20,7 +20,9 @@
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
+use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass;
use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass;
+use Symfony\Component\DependencyInjection\Compiler\ResolveReferencesToAliasesPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;
@@ -900,6 +902,30 @@ public function testCustomHasherWithMigrateFrom()
]);
}
+ public function testAuthenticatorsDecoration()
+ {
+ $container = $this->getRawContainer();
+ $container->setParameter('kernel.debug', true);
+ $container->getCompilerPassConfig()->setOptimizationPasses([
+ new ResolveChildDefinitionsPass(),
+ new DecoratorServicePass(),
+ new ResolveReferencesToAliasesPass(),
+ ]);
+
+ $container->register(TestAuthenticator::class);
+ $container->loadFromExtension('security', [
+ 'firewalls' => ['main' => ['custom_authenticator' => TestAuthenticator::class]],
+ ]);
+ $container->compile();
+
+ /** @var Reference[] $managerAuthenticators */
+ $managerAuthenticators = $container->getDefinition('security.authenticator.manager.main')->getArgument(0);
+ $this->assertCount(1, $managerAuthenticators);
+ $this->assertSame('debug.'.TestAuthenticator::class, (string) reset($managerAuthenticators), 'AuthenticatorManager must be injected traceable authenticators in debug mode.');
+
+ $this->assertTrue($container->hasDefinition(TestAuthenticator::class), 'Original authenticator must still exist in the container so it can be used outside of the AuthenticatorManager’s context.');
+ }
+
protected function getRawContainer()
{
$container = new ContainerBuilder();
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig
index af9f0a4ceaba3..55589c2945d88 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig
@@ -40,6 +40,7 @@
#source .source-content ol li {
margin: 0 0 2px 0;
padding-left: 5px;
+ white-space: preserve nowrap;
}
#source .source-content ol li::marker {
color: var(--color-muted);
diff --git a/src/Symfony/Component/Finder/Comparator/DateComparator.php b/src/Symfony/Component/Finder/Comparator/DateComparator.php
index f3538aaea9566..bcf93cfb6a523 100644
--- a/src/Symfony/Component/Finder/Comparator/DateComparator.php
+++ b/src/Symfony/Component/Finder/Comparator/DateComparator.php
@@ -36,7 +36,7 @@ public function __construct(string $test)
throw new \InvalidArgumentException(\sprintf('"%s" is not a valid date.', $matches[2]));
}
- $operator = $matches[1] ?? '==';
+ $operator = $matches[1] ?: '==';
if ('since' === $operator || 'after' === $operator) {
$operator = '>';
}
diff --git a/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php b/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php
index 47bcc4838bd26..e50b713062638 100644
--- a/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php
+++ b/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php
@@ -59,6 +59,7 @@ public static function getTestData()
['after 2005-10-10', [strtotime('2005-10-15')], [strtotime('2005-10-09')]],
['since 2005-10-10', [strtotime('2005-10-15')], [strtotime('2005-10-09')]],
['!= 2005-10-10', [strtotime('2005-10-11')], [strtotime('2005-10-10')]],
+ ['2005-10-10', [strtotime('2005-10-10')], [strtotime('2005-10-11')]],
];
}
}
diff --git a/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php b/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php
index fe0e0d39cd9d9..c00b8f7dfbfe5 100644
--- a/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php
+++ b/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php
@@ -358,10 +358,10 @@ public static function provideParse(): iterable
'non-special://:@untrusted.com/x' => ['scheme' => 'non-special', 'host' => 'untrusted.com'],
'http:foo.com' => ['scheme' => 'http', 'host' => null],
" :foo.com \n" => null,
- ' foo.com ' => ['scheme' => null, 'host' => null],
+ ' foo.com ' => null,
'a: foo.com' => null,
- 'http://f:21/ b ? d # e ' => ['scheme' => 'http', 'host' => 'f'],
- 'lolscheme:x x#x x' => ['scheme' => 'lolscheme', 'host' => null],
+ 'http://f:21/ b ? d # e ' => null,
+ 'lolscheme:x x#x x' => null,
'http://f:/c' => ['scheme' => 'http', 'host' => 'f'],
'http://f:0/c' => ['scheme' => 'http', 'host' => 'f'],
'http://f:00000000000000/c' => ['scheme' => 'http', 'host' => 'f'],
@@ -434,7 +434,7 @@ public static function provideParse(): iterable
'javascript:example.com/' => ['scheme' => 'javascript', 'host' => null],
'mailto:example.com/' => ['scheme' => 'mailto', 'host' => null],
'/a/b/c' => ['scheme' => null, 'host' => null],
- '/a/ /c' => ['scheme' => null, 'host' => null],
+ '/a/ /c' => null,
'/a%2fc' => ['scheme' => null, 'host' => null],
'/a/%2f/c' => ['scheme' => null, 'host' => null],
'#β' => ['scheme' => null, 'host' => null],
@@ -495,10 +495,10 @@ public static function provideParse(): iterable
'http://example.com/你好你好' => ['scheme' => 'http', 'host' => 'example.com'],
'http://example.com/‥/foo' => ['scheme' => 'http', 'host' => 'example.com'],
"http://example.com/\u{feff}/foo" => ['scheme' => 'http', 'host' => 'example.com'],
- "http://example.com\u{002f}\u{202e}\u{002f}\u{0066}\u{006f}\u{006f}\u{002f}\u{202d}\u{002f}\u{0062}\u{0061}\u{0072}\u{0027}\u{0020}" => ['scheme' => 'http', 'host' => 'example.com'],
+ "http://example.com\u{002f}\u{202e}\u{002f}\u{0066}\u{006f}\u{006f}\u{002f}\u{202d}\u{002f}\u{0062}\u{0061}\u{0072}\u{0027}\u{0020}" => null,
'http://www.google.com/foo?bar=baz#' => ['scheme' => 'http', 'host' => 'www.google.com'],
- 'http://www.google.com/foo?bar=baz# »' => ['scheme' => 'http', 'host' => 'www.google.com'],
- 'data:test# »' => ['scheme' => 'data', 'host' => null],
+ 'http://www.google.com/foo?bar=baz# »' => null,
+ 'data:test# »' => null,
'http://www.google.com' => ['scheme' => 'http', 'host' => 'www.google.com'],
'http://192.0x00A80001' => ['scheme' => 'http', 'host' => '192.0x00A80001'],
'http://www/foo%2Ehtml' => ['scheme' => 'http', 'host' => 'www'],
@@ -706,11 +706,11 @@ public static function provideParse(): iterable
'test-a-colon-slash-slash-b.html' => ['scheme' => null, 'host' => null],
'http://example.org/test?a#bc' => ['scheme' => 'http', 'host' => 'example.org'],
'http:\\/\\/f:b\\/c' => ['scheme' => 'http', 'host' => null],
- 'http:\\/\\/f: \\/c' => ['scheme' => 'http', 'host' => null],
+ 'http:\\/\\/f: \\/c' => null,
'http:\\/\\/f:fifty-two\\/c' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/f:999999\\/c' => ['scheme' => 'http', 'host' => null],
'non-special:\\/\\/f:999999\\/c' => ['scheme' => 'non-special', 'host' => null],
- 'http:\\/\\/f: 21 \\/ b ? d # e ' => ['scheme' => 'http', 'host' => null],
+ 'http:\\/\\/f: 21 \\/ b ? d # e ' => null,
'http:\\/\\/[1::2]:3:4' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/2001::1' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/2001::1]' => ['scheme' => 'http', 'host' => null],
@@ -734,8 +734,8 @@ public static function provideParse(): iterable
'http:@:www.example.com' => ['scheme' => 'http', 'host' => null],
'http:\\/@:www.example.com' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/@:www.example.com' => ['scheme' => 'http', 'host' => null],
- 'http:\\/\\/example example.com' => ['scheme' => 'http', 'host' => null],
- 'http:\\/\\/Goo%20 goo%7C|.com' => ['scheme' => 'http', 'host' => null],
+ 'http:\\/\\/example example.com' => null,
+ 'http:\\/\\/Goo%20 goo%7C|.com' => null,
'http:\\/\\/[]' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/[:]' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/GOO\\u00a0\\u3000goo.com' => ['scheme' => 'http', 'host' => null],
@@ -752,8 +752,8 @@ public static function provideParse(): iterable
'http:\\/\\/hello%00' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/192.168.0.257' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/%3g%78%63%30%2e%30%32%35%30%2E.01' => ['scheme' => 'http', 'host' => null],
- 'http:\\/\\/192.168.0.1 hello' => ['scheme' => 'http', 'host' => null],
- 'https:\\/\\/x x:12' => ['scheme' => 'https', 'host' => null],
+ 'http:\\/\\/192.168.0.1 hello' => null,
+ 'https:\\/\\/x x:12' => null,
'http:\\/\\/[www.google.com]\\/' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/[google.com]' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/[::1.2.3.4x]' => ['scheme' => 'http', 'host' => null],
@@ -763,7 +763,7 @@ public static function provideParse(): iterable
'..\\/i' => ['scheme' => null, 'host' => null],
'\\/i' => ['scheme' => null, 'host' => null],
'sc:\\/\\/\\u0000\\/' => ['scheme' => 'sc', 'host' => null],
- 'sc:\\/\\/ \\/' => ['scheme' => 'sc', 'host' => null],
+ 'sc:\\/\\/ \\/' => null,
'sc:\\/\\/@\\/' => ['scheme' => 'sc', 'host' => null],
'sc:\\/\\/te@s:t@\\/' => ['scheme' => 'sc', 'host' => null],
'sc:\\/\\/:\\/' => ['scheme' => 'sc', 'host' => null],
diff --git a/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php b/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php
index a806981de770f..05d86ba15da8e 100644
--- a/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php
+++ b/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php
@@ -94,7 +94,13 @@ public static function parse(string $url): ?array
}
try {
- return UriString::parse($url);
+ $parsedUrl = UriString::parse($url);
+
+ if (preg_match('/\s/', $url)) {
+ return null;
+ }
+
+ return $parsedUrl;
} catch (SyntaxError) {
return null;
}
diff --git a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php
index 855ed8b2915d2..4fe93645b1b4c 100644
--- a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php
+++ b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php
@@ -138,7 +138,7 @@ public function request(string $method, string $url, array $options = []): Respo
$filterContentHeaders = static function ($h) {
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
};
- $options['header'] = array_filter($options['header'], $filterContentHeaders);
+ $options['headers'] = array_filter($options['headers'], $filterContentHeaders);
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
}
diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php
index d3efa896db8ac..c520e593e371b 100644
--- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php
+++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php
@@ -678,4 +678,26 @@ public function testHeadRequestWithClosureBody()
$this->assertIsArray($vars);
$this->assertSame('HEAD', $vars['REQUEST_METHOD']);
}
+
+ /**
+ * @testWith [301]
+ * [302]
+ * [303]
+ */
+ public function testPostToGetRedirect(int $status)
+ {
+ $p = TestHttpServer::start(8067);
+
+ try {
+ $client = $this->getHttpClient(__FUNCTION__);
+
+ $response = $client->request('POST', 'http://localhost:8057/custom?status=' . $status . '&headers[]=Location%3A%20%2F');
+ $body = $response->toArray();
+ } finally {
+ $p->stop();
+ }
+
+ $this->assertSame('GET', $body['REQUEST_METHOD']);
+ $this->assertSame('/', $body['REQUEST_URI']);
+ }
}
diff --git a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php
index c160dff77492a..ddd83c4960cb0 100644
--- a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php
@@ -175,6 +175,27 @@ public function testNonCallableOnProgressCallback()
$client->request('GET', $url, ['on_progress' => $customCallback]);
}
+ public function testHeadersArePassedOnRedirect()
+ {
+ $ipAddr = '104.26.14.6';
+ $url = sprintf('http://%s/', $ipAddr);
+ $content = 'foo';
+
+ $callback = function ($method, $url, $options) use ($content): MockResponse {
+ $this->assertArrayHasKey('headers', $options);
+ $this->assertNotContains('content-type: application/json', $options['headers']);
+ $this->assertContains('foo: bar', $options['headers']);
+ return new MockResponse($content);
+ };
+ $responses = [
+ new MockResponse('', ['http_code' => 302, 'redirect_url' => 'http://104.26.14.7']),
+ $callback,
+ ];
+ $client = new NoPrivateNetworkHttpClient(new MockHttpClient($responses));
+ $response = $client->request('POST', $url, ['headers' => ['foo' => 'bar', 'content-type' => 'application/json']]);
+ $this->assertEquals($content, $response->getContent());
+ }
+
private function getMockHttpClient(string $ipAddr, string $content)
{
return new MockHttpClient(new MockResponse($content, ['primary_ip' => $ipAddr]));
diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
index 6eddd6c287c85..2c4d831bb9afa 100644
--- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
+++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
@@ -189,7 +189,12 @@ public function prepare(Request $request): static
}
if (!$this->headers->has('Content-Type')) {
- $this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream');
+ $mimeType = null;
+ if (!$this->tempFileObject) {
+ $mimeType = $this->file->getMimeType();
+ }
+
+ $this->headers->set('Content-Type', $mimeType ?: 'application/octet-stream');
}
parent::prepare($request);
diff --git a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php
index 2951cdb514471..1263fa39298ff 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php
@@ -468,4 +468,15 @@ public function testSetChunkSizeTooSmall()
$response->setChunkSize(0);
}
+
+ public function testCreateFromTemporaryFileWithoutMimeType()
+ {
+ $file = new \SplTempFileObject();
+ $file->fwrite('foo,bar');
+
+ $response = new BinaryFileResponse($file);
+ $response->prepare(new Request());
+
+ $this->assertSame('application/octet-stream', $response->headers->get('Content-Type'));
+ }
}
diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
index 1f0ff7cc0f053..a196250e8b23b 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
@@ -46,11 +46,9 @@
class RequestPayloadValueResolver implements ValueResolverInterface, EventSubscriberInterface
{
/**
- * @see \Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT
* @see DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS
*/
private const CONTEXT_DENORMALIZE = [
- 'disable_type_enforcement' => true,
'collect_denormalization_errors' => true,
];
@@ -190,7 +188,7 @@ private function mapQueryString(Request $request, ArgumentMetadata $argument, Ma
return null;
}
- return $this->serializer->denormalize($data, $argument->getType(), null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
+ return $this->serializer->denormalize($data, $argument->getType(), 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
}
private function mapRequestPayload(Request $request, ArgumentMetadata $argument, MapRequestPayload $attribute): object|array|null
@@ -210,7 +208,7 @@ private function mapRequestPayload(Request $request, ArgumentMetadata $argument,
}
if ($data = $request->request->all()) {
- return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
+ return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
}
if ('' === ($data = $request->getContent()) && ($argument->isNullable() || $argument->hasDefaultValue())) {
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index 82e6dbfbd0c79..387b51c8a4fce 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -73,11 +73,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
private static array $freshCache = [];
- public const VERSION = '7.2.1';
- public const VERSION_ID = 70201;
+ public const VERSION = '7.2.2';
+ public const VERSION_ID = 70202;
public const MAJOR_VERSION = 7;
public const MINOR_VERSION = 2;
- public const RELEASE_VERSION = 1;
+ public const RELEASE_VERSION = 2;
public const EXTRA_VERSION = '';
public const END_OF_MAINTENANCE = '07/2025';
diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
index 8b26767f9ea94..8ed4adfe00567 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
@@ -22,6 +22,7 @@
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -395,6 +396,38 @@ public function testQueryStringValidationPassed()
$this->assertEquals([$payload], $event->getArguments());
}
+ public function testQueryStringParameterTypeMismatch()
+ {
+ $query = ['price' => 'not a float'];
+
+ $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
+ $serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]);
+
+ $validator = $this->createMock(ValidatorInterface::class);
+ $validator->expects($this->never())->method('validate');
+
+ $resolver = new RequestPayloadValueResolver($serializer, $validator);
+
+ $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [
+ MapQueryString::class => new MapQueryString(),
+ ]);
+
+ $request = Request::create('/', 'GET', $query);
+
+ $kernel = $this->createMock(HttpKernelInterface::class);
+ $arguments = $resolver->resolve($request, $argument);
+ $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
+
+ try {
+ $resolver->onKernelControllerArguments($event);
+ $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
+ } catch (HttpException $e) {
+ $validationFailedException = $e->getPrevious();
+ $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException);
+ $this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage());
+ }
+ }
+
public function testRequestInputValidationPassed()
{
$input = ['price' => '50'];
@@ -457,6 +490,38 @@ public function testRequestArrayDenormalization()
$this->assertEquals([$payload], $event->getArguments());
}
+ public function testRequestInputTypeMismatch()
+ {
+ $input = ['price' => 'not a float'];
+
+ $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
+ $serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]);
+
+ $validator = $this->createMock(ValidatorInterface::class);
+ $validator->expects($this->never())->method('validate');
+
+ $resolver = new RequestPayloadValueResolver($serializer, $validator);
+
+ $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [
+ MapRequestPayload::class => new MapRequestPayload(),
+ ]);
+
+ $request = Request::create('/', 'POST', $input);
+
+ $kernel = $this->createMock(HttpKernelInterface::class);
+ $arguments = $resolver->resolve($request, $argument);
+ $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
+
+ try {
+ $resolver->onKernelControllerArguments($event);
+ $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
+ } catch (HttpException $e) {
+ $validationFailedException = $e->getPrevious();
+ $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException);
+ $this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage());
+ }
+ }
+
public function testItThrowsOnMissingAttributeType()
{
$serializer = new Serializer();
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php
index fa9885d2753cd..43c740ee12b98 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php
@@ -60,8 +60,8 @@ public function testRenderControllerReference()
$reference = new ControllerReference('main_controller', [], []);
$altReference = new ControllerReference('alt_controller', [], []);
- $this->assertEquals(
- '',
+ $this->assertMatchesRegularExpression(
+ '#^$#',
$strategy->render($reference, $request, ['alt' => $altReference])->getContent()
);
}
@@ -78,8 +78,8 @@ public function testRenderControllerReferenceWithAbsoluteUri()
$reference = new ControllerReference('main_controller', [], []);
$altReference = new ControllerReference('alt_controller', [], []);
- $this->assertSame(
- '',
+ $this->assertMatchesRegularExpression(
+ '#^$#',
$strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent()
);
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php
index f74887ade36f4..8e4b59e5feeb9 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php
@@ -32,7 +32,7 @@ public function testRenderWithControllerAndSigner()
{
$strategy = new HIncludeFragmentRenderer(null, new UriSigner('foo'));
- $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent());
+ $this->assertMatchesRegularExpression('#^$#', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent());
}
public function testRenderWithUri()
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php
index 4af00f9f75137..7fd04c5a5b0b7 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php
@@ -51,8 +51,8 @@ public function testRenderControllerReference()
$reference = new ControllerReference('main_controller', [], []);
$altReference = new ControllerReference('alt_controller', [], []);
- $this->assertEquals(
- '',
+ $this->assertMatchesRegularExpression(
+ '{^$}',
$strategy->render($reference, $request, ['alt' => $altReference])->getContent()
);
}
@@ -69,8 +69,8 @@ public function testRenderControllerReferenceWithAbsoluteUri()
$reference = new ControllerReference('main_controller', [], []);
$altReference = new ControllerReference('alt_controller', [], []);
- $this->assertSame(
- '',
+ $this->assertMatchesRegularExpression(
+ '{^$}',
$strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent()
);
}
diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php
index eaacfa4b60b99..c4b7e904a544e 100644
--- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php
+++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php
@@ -363,4 +363,25 @@ public function testKeepaliveWhenABeanstalkdExceptionOccurs()
$this->expectExceptionObject(new TransportException($exception->getMessage(), 0, $exception));
$connection->keepalive((string) $id);
}
+
+ public function testSendWithRoundedDelay()
+ {
+ $tube = 'xyz';
+ $body = 'foo';
+ $headers = ['test' => 'bar'];
+ $delay = 920;
+ $expectedDelay = 0;
+
+ $client = $this->createMock(PheanstalkInterface::class);
+ $client->expects($this->once())->method('useTube')->with($tube)->willReturn($client);
+ $client->expects($this->once())->method('put')->with(
+ $this->anything(),
+ $this->anything(),
+ $expectedDelay,
+ $this->anything(),
+ );
+
+ $connection = new Connection(['tube_name' => $tube], $client);
+ $connection->send($body, $headers, $delay);
+ }
}
diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php
index c2c5cd7ee49f8..0f2c2c555a4fb 100644
--- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php
+++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php
@@ -124,7 +124,7 @@ public function send(string $body, array $headers, int $delay = 0): string
$job = $this->client->useTube($this->tube)->put(
$message,
PheanstalkInterface::DEFAULT_PRIORITY,
- $delay / 1000,
+ (int) ($delay / 1000),
$this->ttr
);
} catch (Exception $exception) {
diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php
index f1f5fbeef8d62..49ebf00dbdd5d 100644
--- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php
+++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php
@@ -15,7 +15,6 @@
use Doctrine\DBAL\Connection as DBALConnection;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Platforms\MariaDb1060Platform;
use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQL57Platform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
@@ -591,9 +590,16 @@ class_exists(MySQLPlatform::class) ? new MySQLPlatform() : new MySQL57Platform()
'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE',
];
- if (class_exists(MariaDb1060Platform::class)) {
+ if (interface_exists(DBALException::class)) {
+ // DBAL 4+
+ $mariaDbPlatformClass = 'Doctrine\DBAL\Platforms\MariaDB1060Platform';
+ } else {
+ $mariaDbPlatformClass = 'Doctrine\DBAL\Platforms\MariaDb1060Platform';
+ }
+
+ if (class_exists($mariaDbPlatformClass)) {
yield 'MariaDB106' => [
- new MariaDb1060Platform(),
+ new $mariaDbPlatformClass(),
'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED',
];
}
diff --git a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php
index 8de070dc046c9..d006e32483896 100644
--- a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php
+++ b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php
@@ -15,8 +15,6 @@
* The property read info tells how a property can be read.
*
* @author Joel Wurtz
- *
- * @internal
*/
final class PropertyReadInfo
{
diff --git a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php
index 6bc7abcdf849e..81ce7eda6d5b0 100644
--- a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php
+++ b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php
@@ -15,8 +15,6 @@
* The write mutator defines how a property can be written.
*
* @author Joel Wurtz
- *
- * @internal
*/
final class PropertyWriteInfo
{
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
index 6248e4966dc15..0d77497c2e1da 100644
--- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
+++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
@@ -14,15 +14,18 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
+use Symfony\Component\PropertyInfo\Tests\Fixtures\Clazz;
use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyCollection;
+use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyGeneric;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyNamespace;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyPropertyAndGetterWithDifferentTypes;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyUnionType;
+use Symfony\Component\PropertyInfo\Tests\Fixtures\IFace;
use Symfony\Component\PropertyInfo\Tests\Fixtures\IntRangeDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy;
@@ -552,6 +555,77 @@ public static function allowPrivateAccessLegacyProvider(): array
];
}
+ /**
+ * @param list $expectedTypes
+ *
+ * @dataProvider legacyGenericsProvider
+ */
+ public function testGenericsLegacy(string $property, array $expectedTypes)
+ {
+ $this->assertEquals($expectedTypes, $this->extractor->getTypes(DummyGeneric::class, $property));
+ }
+
+ /**
+ * @return iterable}>
+ */
+ public static function legacyGenericsProvider(): iterable
+ {
+ yield [
+ 'basicClass',
+ [
+ new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Clazz::class,
+ collectionValueType: new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Dummy::class,
+ )
+ ),
+ ],
+ ];
+ yield [
+ 'nullableClass',
+ [
+ new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Clazz::class,
+ nullable: true,
+ collectionValueType: new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Dummy::class,
+ )
+ ),
+ ],
+ ];
+ yield [
+ 'basicInterface',
+ [
+ new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: IFace::class,
+ collectionValueType: new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Dummy::class,
+ )
+ ),
+ ],
+ ];
+ yield [
+ 'nullableInterface',
+ [
+ new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: IFace::class,
+ nullable: true,
+ collectionValueType: new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Dummy::class,
+ )
+ ),
+ ],
+ ];
+ }
+
/**
* @dataProvider typesProvider
*/
@@ -968,7 +1042,41 @@ public static function allowPrivateAccessProvider(): array
public function testGenericInterface()
{
- $this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface'));
+ $this->assertEquals(
+ Type::generic(Type::enum(\BackedEnum::class), Type::string()),
+ $this->extractor->getType(Dummy::class, 'genericInterface'),
+ );
+ }
+
+ /**
+ * @dataProvider genericsProvider
+ */
+ public function testGenerics(string $property, Type $expectedType)
+ {
+ $this->assertEquals($expectedType, $this->extractor->getType(DummyGeneric::class, $property));
+ }
+
+ /**
+ * @return iterable
+ */
+ public static function genericsProvider(): iterable
+ {
+ yield [
+ 'basicClass',
+ Type::generic(Type::object(Clazz::class), Type::object(Dummy::class)),
+ ];
+ yield [
+ 'nullableClass',
+ Type::nullable(Type::generic(Type::object(Clazz::class), Type::object(Dummy::class))),
+ ];
+ yield [
+ 'basicInterface',
+ Type::generic(Type::object(IFace::class), Type::object(Dummy::class)),
+ ];
+ yield [
+ 'nullableInterface',
+ Type::nullable(Type::generic(Type::object(IFace::class), Type::object(Dummy::class))),
+ ];
}
}
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php
new file mode 100644
index 0000000000000..5863fbfc95450
--- /dev/null
+++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
+
+interface IFace {}
+
+class Clazz {}
+
+class DummyGeneric
+{
+
+ /**
+ * @var Clazz
+ */
+ public $basicClass;
+
+ /**
+ * @var ?Clazz
+ */
+ public $nullableClass;
+
+ /**
+ * @var IFace
+ */
+ public $basicInterface;
+
+ /**
+ * @var ?IFace
+ */
+ public $nullableInterface;
+
+}
diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php
index 6d06d8b8a9f16..924d74e3aec1a 100644
--- a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php
+++ b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php
@@ -128,7 +128,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
$collection = $mainType->isCollection() || \is_a($mainType->getClassName(), \Traversable::class, true) || \is_a($mainType->getClassName(), \ArrayAccess::class, true);
// it's safer to fall back to other extractors if the generic type is too abstract
- if (!$collection && !class_exists($mainType->getClassName())) {
+ if (!$collection && !class_exists($mainType->getClassName()) && !interface_exists($mainType->getClassName(), false)) {
return [];
}
diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json
index 70a57ef06ced0..c033ca51befdc 100644
--- a/src/Symfony/Component/PropertyInfo/composer.json
+++ b/src/Symfony/Component/PropertyInfo/composer.json
@@ -25,7 +25,7 @@
"require": {
"php": ">=8.2",
"symfony/string": "^6.4|^7.0",
- "symfony/type-info": "^7.1"
+ "symfony/type-info": "~7.1.9|^7.2.2"
},
"require-dev": {
"symfony/serializer": "^6.4|^7.0",
diff --git a/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php b/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php
index dc10d30307484..f8724e59247c1 100644
--- a/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php
+++ b/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php
@@ -105,9 +105,9 @@ public static function provideForToString()
/**
* @dataProvider providerGetNextRunDates
*/
- public function testGetNextRunDates(\DateTimeImmutable $from, TriggerInterface $trigger, array $expected, int $count = 0)
+ public function testGetNextRunDates(\DateTimeImmutable $from, TriggerInterface $trigger, array $expected, int $count)
{
- $this->assertEquals($expected, $this->getNextRunDates($from, $trigger, $count ?? \count($expected)));
+ $this->assertEquals($expected, $this->getNextRunDates($from, $trigger, $count));
}
public static function providerGetNextRunDates(): iterable
diff --git a/src/Symfony/Component/Security/Csrf/SameOriginCsrfTokenManager.php b/src/Symfony/Component/Security/Csrf/SameOriginCsrfTokenManager.php
index 9ef61964bfe1e..0c95208c0f580 100644
--- a/src/Symfony/Component/Security/Csrf/SameOriginCsrfTokenManager.php
+++ b/src/Symfony/Component/Security/Csrf/SameOriginCsrfTokenManager.php
@@ -227,9 +227,21 @@ public function onKernelResponse(ResponseEvent $event): void
*/
private function isValidOrigin(Request $request): ?bool
{
- $source = $request->headers->get('Origin') ?? $request->headers->get('Referer') ?? 'null';
+ $target = $request->getSchemeAndHttpHost().'/';
+ $source = 'null';
- return 'null' === $source ? null : str_starts_with($source.'/', $request->getSchemeAndHttpHost().'/');
+ foreach (['Origin', 'Referer'] as $header) {
+ if (!$request->headers->has($header)) {
+ continue;
+ }
+ $source = $request->headers->get($header);
+
+ if (str_starts_with($source.'/', $target)) {
+ return true;
+ }
+ }
+
+ return 'null' === $source ? null : false;
}
/**
diff --git a/src/Symfony/Component/Security/Csrf/Tests/SameOriginCsrfTokenManagerTest.php b/src/Symfony/Component/Security/Csrf/Tests/SameOriginCsrfTokenManagerTest.php
index 1ad17b80e0549..eae31deee379c 100644
--- a/src/Symfony/Component/Security/Csrf/Tests/SameOriginCsrfTokenManagerTest.php
+++ b/src/Symfony/Component/Security/Csrf/Tests/SameOriginCsrfTokenManagerTest.php
@@ -100,6 +100,20 @@ public function testValidOrigin()
$this->assertSame(1 << 8, $request->attributes->get('csrf-token'));
}
+ public function testValidRefererInvalidOrigin()
+ {
+ $request = new Request();
+ $request->headers->set('Origin', 'http://localhost:1234');
+ $request->headers->set('Referer', $request->getSchemeAndHttpHost());
+ $this->requestStack->push($request);
+
+ $token = new CsrfToken('test_token', str_repeat('a', 24));
+
+ $this->logger->expects($this->once())->method('debug')->with('CSRF validation accepted using origin info.');
+ $this->assertTrue($this->csrfTokenManager->isTokenValid($token));
+ $this->assertSame(1 << 8, $request->attributes->get('csrf-token'));
+ }
+
public function testValidOriginAfterDoubleSubmit()
{
$session = $this->createMock(Session::class);
diff --git a/src/Symfony/Component/Stopwatch/Stopwatch.php b/src/Symfony/Component/Stopwatch/Stopwatch.php
index 50ac6574fd436..8961507fa07db 100644
--- a/src/Symfony/Component/Stopwatch/Stopwatch.php
+++ b/src/Symfony/Component/Stopwatch/Stopwatch.php
@@ -140,7 +140,7 @@ public function getEvent(string $name): StopwatchEvent
*/
public function getSectionEvents(string $id): array
{
- return $this->sections[$id]->getEvents() ?? [];
+ return isset($this->sections[$id]) ? $this->sections[$id]->getEvents() : [];
}
/**
diff --git a/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php b/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php
index f1e2270018447..f9b532efe1fe4 100644
--- a/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php
+++ b/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php
@@ -187,4 +187,9 @@ public function testReset()
$this->assertEquals(new Stopwatch(), $stopwatch);
}
+
+ public function testShouldReturnEmptyArrayWhenSectionMissing()
+ {
+ $this->assertSame([], (new Stopwatch())->getSectionEvents('missing'));
+ }
}
diff --git a/src/Symfony/Component/Translation/PseudoLocalizationTranslator.php b/src/Symfony/Component/Translation/PseudoLocalizationTranslator.php
index 5d56d2cc11bd6..fe5b0adc25216 100644
--- a/src/Symfony/Component/Translation/PseudoLocalizationTranslator.php
+++ b/src/Symfony/Component/Translation/PseudoLocalizationTranslator.php
@@ -11,12 +11,13 @@
namespace Symfony\Component\Translation;
+use Symfony\Component\Translation\Exception\LogicException;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* This translator should only be used in a development environment.
*/
-final class PseudoLocalizationTranslator implements TranslatorInterface
+final class PseudoLocalizationTranslator implements TranslatorInterface, TranslatorBagInterface
{
private const EXPANSION_CHARACTER = '~';
@@ -115,6 +116,24 @@ public function getLocale(): string
return $this->translator->getLocale();
}
+ public function getCatalogue(?string $locale = null): MessageCatalogueInterface
+ {
+ if (!$this->translator instanceof TranslatorBagInterface) {
+ throw new LogicException(\sprintf('The "%s()" method cannot be called as the wrapped translator class "%s" does not implement the "%s".', __METHOD__, $this->translator::class, TranslatorBagInterface::class));
+ }
+
+ return $this->translator->getCatalogue($locale);
+ }
+
+ public function getCatalogues(): array
+ {
+ if (!$this->translator instanceof TranslatorBagInterface) {
+ throw new LogicException(\sprintf('The "%s()" method cannot be called as the wrapped translator class "%s" does not implement the "%s".', __METHOD__, $this->translator::class, TranslatorBagInterface::class));
+ }
+
+ return $this->translator->getCatalogues();
+ }
+
private function getParts(string $originalTrans): array
{
if (!$this->parseHTML) {
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
index b0ad600085e14..9320987c6baed 100644
--- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
@@ -16,7 +16,9 @@
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyCollection;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeContext\TypeContext;
@@ -138,6 +140,8 @@ public static function resolveDataProvider(): iterable
yield [Type::object(Dummy::class), 'static', $typeContextFactory->createFromClassName(Dummy::class, AbstractDummy::class)];
yield [Type::object(AbstractDummy::class), 'parent', $typeContextFactory->createFromClassName(Dummy::class)];
yield [Type::object(Dummy::class), 'Dummy', $typeContextFactory->createFromClassName(Dummy::class)];
+ yield [Type::enum(DummyEnum::class), 'DummyEnum', $typeContextFactory->createFromClassName(DummyEnum::class)];
+ yield [Type::enum(DummyBackedEnum::class), 'DummyBackedEnum', $typeContextFactory->createFromClassName(DummyBackedEnum::class)];
yield [Type::template('T', Type::union(Type::int(), Type::string())), 'T', $typeContextFactory->createFromClassName(DummyWithTemplates::class)];
yield [Type::template('V'), 'V', $typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithTemplates::class, 'getPrice'))];
@@ -151,6 +155,8 @@ public static function resolveDataProvider(): iterable
// union
yield [Type::union(Type::int(), Type::string()), 'int|string'];
+ yield [Type::mixed(), 'int|mixed'];
+ yield [Type::mixed(), 'mixed|int'];
// intersection
yield [Type::intersection(Type::object(\DateTime::class), Type::object(\Stringable::class)), \DateTime::class.'&'.\Stringable::class];
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php
index 01b32b66f5777..23e6b3a2860a5 100644
--- a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php
@@ -25,11 +25,6 @@
*/
final class ReflectionTypeResolver implements TypeResolverInterface
{
- /**
- * @var array
- */
- private static array $reflectionEnumCache = [];
-
public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type
{
if ($subject instanceof \ReflectionUnionType) {
@@ -81,11 +76,7 @@ public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type
default => $identifier,
};
- if (is_subclass_of($className, \BackedEnum::class)) {
- $reflectionEnum = (self::$reflectionEnumCache[$className] ??= new \ReflectionEnum($className));
- $backingType = $this->resolve($reflectionEnum->getBackingType(), $typeContext);
- $type = Type::enum($className, $backingType);
- } elseif (is_subclass_of($className, \UnitEnum::class)) {
+ if (is_subclass_of($className, \UnitEnum::class)) {
$type = Type::enum($className);
} else {
$type = Type::object($className);
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
index 4809e9b83bf7e..a172d388a8722 100644
--- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
@@ -223,7 +223,19 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ
}
if ($node instanceof UnionTypeNode) {
- return Type::union(...array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->types));
+ $types = [];
+
+ foreach ($node->types as $nodeType) {
+ $type = $this->getTypeFromNode($nodeType, $typeContext);
+
+ if ($type instanceof BuiltinType && TypeIdentifier::MIXED === $type->getTypeIdentifier()) {
+ return Type::mixed();
+ }
+
+ $types[] = $type;
+ }
+
+ return Type::union(...$types);
}
if ($node instanceof IntersectionTypeNode) {
@@ -246,14 +258,16 @@ private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeC
try {
new \ReflectionClass($className);
self::$classExistCache[$className] = true;
-
- return Type::object($className);
} catch (\Throwable) {
}
}
}
if (self::$classExistCache[$className]) {
+ if (is_subclass_of($className, \UnitEnum::class)) {
+ return Type::enum($className);
+ }
+
return Type::object($className);
}
diff --git a/src/Symfony/Component/Validator/Constraints/Count.php b/src/Symfony/Component/Validator/Constraints/Count.php
index 175e07ebb8fdd..38ea8d6e74e71 100644
--- a/src/Symfony/Component/Validator/Constraints/Count.php
+++ b/src/Symfony/Component/Validator/Constraints/Count.php
@@ -45,7 +45,7 @@ class Count extends Constraint
/**
* @param int<0, max>|array|null $exactly The exact expected number of elements
* @param int<0, max>|null $min Minimum expected number of elements
- * @param positive-int|null $max Maximum expected number of elements
+ * @param int<0, max>|null $max Maximum expected number of elements
* @param positive-int|null $divisibleBy The number the collection count should be divisible by
* @param string[]|null $groups
* @param array $options
diff --git a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php
index 408088c7ef709..8977fd2210809 100644
--- a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php
@@ -47,11 +47,15 @@ public function validate(mixed $value, Constraint $constraint): void
}
if (\in_array($element, $collectionElements, true)) {
- $this->context->buildViolation($constraint->message)
- ->atPath("[$index]".(null !== $constraint->errorPath ? ".{$constraint->errorPath}" : ''))
+ $violationBuilder = $this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($element))
- ->setCode(Unique::IS_NOT_UNIQUE)
- ->addViolation();
+ ->setCode(Unique::IS_NOT_UNIQUE);
+
+ if (null !== $constraint->errorPath) {
+ $violationBuilder->atPath("[$index].{$constraint->errorPath}");
+ }
+
+ $violationBuilder->addViolation();
return;
}
diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf
index 3977f37433060..485d69add1ee8 100644
--- a/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf
+++ b/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf
@@ -444,27 +444,27 @@
This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words.
- This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words.
+ این مقدار بسیار کوتاه است. باید حداقل یک کلمه داشته باشد.|این مقدار بسیار کوتاه است. باید حداقل {{ min }} کلمه داشته باشد.
This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less.
- This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less.
+ این مقدار بیش از حد طولانی است. باید فقط یک کلمه باشد.|این مقدار بیش از حد طولانی است. باید حداکثر {{ max }} کلمه داشته باشد.
This value does not represent a valid week in the ISO 8601 format.
- This value does not represent a valid week in the ISO 8601 format.
+ این مقدار یک هفته معتبر در قالب ISO 8601 را نشان نمیدهد.
This value is not a valid week.
- This value is not a valid week.
+ این مقدار یک هفته معتبر نیست.
This value should not be before week "{{ min }}".
- This value should not be before week "{{ min }}".
+ این مقدار نباید قبل از هفته "{{ min }}" باشد.
This value should not be after week "{{ max }}".
- This value should not be after week "{{ max }}".
+ این مقدار نباید بعد از هفته "{{ max }}" باشد.