From 80eb60974da1e7dde9214e644ca668342364ceff Mon Sep 17 00:00:00 2001 From: Thomas Bibaut Date: Thu, 19 Dec 2024 14:46:32 +0100 Subject: [PATCH 01/63] [DependencyInjection] Fix env default processor with scalar node --- .../Compiler/ValidateEnvPlaceholdersPass.php | 59 +++++++++++++++---- .../ValidateEnvPlaceholdersPassTest.php | 30 ++++++++++ 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ValidateEnvPlaceholdersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ValidateEnvPlaceholdersPass.php index 2d6542660b39c..75bd6097deee6 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ValidateEnvPlaceholdersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ValidateEnvPlaceholdersPass.php @@ -49,17 +49,8 @@ public function process(ContainerBuilder $container) $defaultBag = new ParameterBag($resolvingBag->all()); $envTypes = $resolvingBag->getProvidedTypes(); foreach ($resolvingBag->getEnvPlaceholders() + $resolvingBag->getUnusedEnvPlaceholders() as $env => $placeholders) { - $values = []; - if (false === $i = strpos($env, ':')) { - $default = $defaultBag->has("env($env)") ? $defaultBag->get("env($env)") : self::TYPE_FIXTURES['string']; - $defaultType = null !== $default ? get_debug_type($default) : 'string'; - $values[$defaultType] = $default; - } else { - $prefix = substr($env, 0, $i); - foreach ($envTypes[$prefix] ?? ['string'] as $type) { - $values[$type] = self::TYPE_FIXTURES[$type] ?? null; - } - } + $values = $this->getPlaceholderValues($env, $defaultBag, $envTypes); + foreach ($placeholders as $placeholder) { BaseNode::setPlaceholder($placeholder, $values); } @@ -100,4 +91,50 @@ public function getExtensionConfig(): array $this->extensionConfig = []; } } + + /** + * @param array> $envTypes + * + * @return array + */ + private function getPlaceholderValues(string $env, ParameterBag $defaultBag, array $envTypes): array + { + if (false === $i = strpos($env, ':')) { + [$default, $defaultType] = $this->getParameterDefaultAndDefaultType("env($env)", $defaultBag); + + return [$defaultType => $default]; + } + + $prefix = substr($env, 0, $i); + if ('default' === $prefix) { + $parts = explode(':', $env); + array_shift($parts); // Remove 'default' prefix + $parameter = array_shift($parts); // Retrieve and remove parameter + + [$defaultParameter, $defaultParameterType] = $this->getParameterDefaultAndDefaultType($parameter, $defaultBag); + + return [ + $defaultParameterType => $defaultParameter, + ...$this->getPlaceholderValues(implode(':', $parts), $defaultBag, $envTypes), + ]; + } + + $values = []; + foreach ($envTypes[$prefix] ?? ['string'] as $type) { + $values[$type] = self::TYPE_FIXTURES[$type] ?? null; + } + + return $values; + } + + /** + * @return array{0: string, 1: string} + */ + private function getParameterDefaultAndDefaultType(string $name, ParameterBag $defaultBag): array + { + $default = $defaultBag->has($name) ? $defaultBag->get($name) : self::TYPE_FIXTURES['string']; + $defaultType = null !== $default ? get_debug_type($default) : 'string'; + + return [$default, $defaultType]; + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateEnvPlaceholdersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateEnvPlaceholdersPassTest.php index 8c5c4cc32323e..17ef87c3fffad 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateEnvPlaceholdersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateEnvPlaceholdersPassTest.php @@ -73,6 +73,36 @@ public function testDefaultEnvWithoutPrefixIsValidatedInConfig() $this->doProcess($container); } + public function testDefaultProcessorWithScalarNode() + { + $container = new ContainerBuilder(); + $container->setParameter('parameter_int', 12134); + $container->setParameter('env(FLOATISH)', 4.2); + $container->registerExtension($ext = new EnvExtension()); + $container->prependExtensionConfig('env_extension', $expected = [ + 'scalar_node' => '%env(default:parameter_int:FLOATISH)%', + ]); + + $this->doProcess($container); + $this->assertSame($expected, $container->resolveEnvPlaceholders($ext->getConfig())); + } + + public function testDefaultProcessorAndAnotherProcessorWithScalarNode() + { + $this->expectException(InvalidTypeException::class); + $this->expectExceptionMessageMatches('/^Invalid type for path "env_extension\.scalar_node"\. Expected one of "bool", "int", "float", "string", but got one of "int", "array"\.$/'); + + $container = new ContainerBuilder(); + $container->setParameter('parameter_int', 12134); + $container->setParameter('env(JSON)', '{ "foo": "bar" }'); + $container->registerExtension($ext = new EnvExtension()); + $container->prependExtensionConfig('env_extension', [ + 'scalar_node' => '%env(default:parameter_int:json:JSON)%', + ]); + + $this->doProcess($container); + } + public function testEnvsAreValidatedInConfigWithInvalidPlaceholder() { $this->expectException(InvalidTypeException::class); From a8516b7184f96ffa497d39eef869f5acda52d979 Mon Sep 17 00:00:00 2001 From: PHAS Developer <110562019+phasdev@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:24:43 +0000 Subject: [PATCH 02/63] [Security] Return null instead of empty username to fix deprecation notice --- .../Security/Http/Authenticator/RemoteUserAuthenticator.php | 2 +- .../Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php index 958eeaeaec227..39649666ccace 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php @@ -45,6 +45,6 @@ protected function extractUsername(Request $request): ?string throw new BadCredentialsException(sprintf('User key was not found: "%s".', $this->userKey)); } - return $request->server->get($this->userKey); + return $request->server->get($this->userKey) ?: null; } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php index 5119f8ce09e74..b94f90884e2e0 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php @@ -36,6 +36,7 @@ public function testSupportNoUser() $authenticator = new RemoteUserAuthenticator(new InMemoryUserProvider(), new TokenStorage(), 'main'); $this->assertFalse($authenticator->supports($this->createRequest([]))); + $this->assertFalse($authenticator->supports($this->createRequest(['REMOTE_USER' => '']))); } public function testSupportTokenStorageWithToken() From 6cae9410ed839f001ba1342404df3f51dacc13e7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 29 Jan 2025 08:11:05 +0100 Subject: [PATCH 03/63] Update CHANGELOG for 6.4.18 --- CHANGELOG-6.4.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGELOG-6.4.md b/CHANGELOG-6.4.md index 56df1b333f50b..883cd3cd7dd62 100644 --- a/CHANGELOG-6.4.md +++ b/CHANGELOG-6.4.md @@ -7,6 +7,53 @@ in 6.4 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/v6.4.0...v6.4.1 +* 6.4.18 (2025-01-29) + + * bug #58889 [Serializer] Handle default context in Serializer (Valmonzo) + * bug #59631 [HttpClient] Fix processing a NativeResponse after its client has been reset (Jean-Beru) + * bug #59590 [Security] Throw an explicit error when refreshing a token with a null user (alexandre-daubois) + * bug #59625 [FrameworkBundle] Add missing `not-compromised-password` entry in XSD (alexandre-daubois) + * bug #59610 [Mailer] Ensure TransportExceptionInterface populates stream debug data (bytestream) + * bug #59598 [Mime] Fix body validity check in `Email` when using `Message::setBody()` (alexandre-daubois) + * bug #59544 [AssetMapper] Fix CssCompiler matches url in comments (smnandre) + * bug #59575 [DoctrineBridge] Add support for doctrine/persistence 4 (greg0ire) + * bug #59399 [DomCrawler] Make `ChoiceFormField::isDisabled` return `true` for unchecked disabled checkboxes (MatTheCat) + * bug #59581 [Cache] Don't clear system caches on `cache:clear` (nicolas-grekas) + * bug #59579 [FrameworkBundle] Fix patching refs to the tmp warmup dir in files generated by optional cache warmers (nicolas-grekas) + * bug #59525 [HtmlSanitizer] Fix access to undefined keys in UrlSanitizer (Antoine Beyet) + * bug #59538 [VarDumper] fix dumped markup (xabbuh) + * bug #59515 [FrameworkBundle] Fix wiring ConsoleProfilerListener (nicolas-grekas) + * bug #59486 [Validator] Update sr_Cyrl 120:This value is not a valid slug. (kaznovac) + * bug #59403 [FrameworkBundle][HttpFoundation] Reset Request's formats using the service resetter (nicolas-grekas) + * bug #59404 [Mailer] Fix SMTP stream EOF handling on Windows by using feof() (skmedix) + * bug #59390 [VarDumper] Fix blank strings display (MatTheCat) + * bug #59446 [Routing] Fix configuring a single route's hosts (MatTheCat) + * bug #58901 [HttpClient] Ignore RuntimeExceptions thrown when rewinding the PSR-7 created in HttplugWaitLoop::createPsr7Response (KurtThiemann) + * bug #59046 [HttpClient] Fix Undefined array key `connection` (PhilETaylor) + * bug #59055 [HttpFoundation] Fixed `IpUtils::anonymize` exception when using IPv6 link-local addresses with RFC4007 scoping (jbtronics) + * bug #59256 [Mailer] Fix Sendmail memory leak (rch7) + * bug #59375 [RemoteEvent][Webhook] fix SendgridPayloadConverter category support (ericabouaf) + * bug #59367 [PropertyInfo] Make sure that SerializerExtractor returns null for invalid class metadata (wuchen90) + * bug #59376 [RemoteEvent][Webhook] Fix `SendgridRequestParser` and `SendgridPayloadConverter` (ericabouaf) + * bug #59381 [Yaml] fix inline notation with inline comment (alexpott) + * bug #59352 [Messenger] Fix `TransportMessageIdStamp` not always added (HypeMC) + * bug #59185 [DoctrineBridge] Fix compatibility to Doctrine persistence 2.5 in Doctrine Bridge 6.4 to avoid Projects stuck on 6.3 (alexander-schranz) + * bug #59245 [PropertyInfo] Fix add missing composer conflict (mtarld) + * bug #59292 [WebProfilerBundle] Fix event delegation on links inside toggles (MatTheCat) + * bug #59362 [Doctrine][Messenger] Prevents multiple TransportMessageIdStamp being stored in envelope (rtreffler) + * bug #59323 [Serializer] Fix exception thrown by `YamlEncoder` (VincentLanglet) + * bug #59293 [AssetMapper] Fix JavaScript compiler creates self-referencing imports (smnandre) + * bug #59349 [Yaml] reject inline notations followed by invalid content (xabbuh) + * bug #59363 [VarDumper] Fix displaying closure's "this" from anonymous classes (nicolas-grekas) + * bug #59364 [ErrorHandler] Don't trigger "internal" deprecations for anonymous LazyClosure instances (nicolas-grekas) + * bug #59221 [PropertyAccess] Fix compatibility with PHP 8.4 asymmetric visibility (Florian-Merle) + * bug #59357 [HttpKernel] Don't override existing `LoggerInterface` autowiring alias in `LoggerPass` (nicolas-grekas) + * bug #59347 [Security] Fix triggering session tracking from ContextListener (nicolas-grekas) + * bug #59188 [HttpClient] Fix `reset()` not called on decorated clients (HypeMC) + * bug #59343 [Security] Adjust parameter order in exception message (Link1515) + * bug #59312 [Yaml] Fix parsing of unquoted strings in Parser::lexUnquotedString() to ignore spaces (Link1515) + * bug #59334 [ErrorHandler] [A11y] Simple proposal for color updates on error stack traces against colorblindness (DocFX) + * 6.4.17 (2024-12-31) * bug #59304 [PropertyInfo] Remove ``@internal`` from `PropertyReadInfo` and `PropertyWriteInfo` (Dario Guarracino) From 12114352cb3a05b036f6bde4c14633c3a89f1b5e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 29 Jan 2025 08:25:54 +0100 Subject: [PATCH 04/63] Update CONTRIBUTORS for 6.4.18 --- CONTRIBUTORS.md | 113 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 37 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d0472fa4bd167..8af7f51d72c7d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -12,8 +12,8 @@ The Symfony Connect username in parenthesis allows to get more information - Robin Chalas (chalas_r) - Tobias Schultze (tobion) - Grégoire Pineau (lyrixx) - - Thomas Calvet (fancyweb) - Alexandre Daubois (alexandre-daubois) + - Thomas Calvet (fancyweb) - Christophe Coevoet (stof) - Wouter de Jong (wouterj) - Jordi Boggiano (seldaek) @@ -55,8 +55,8 @@ The Symfony Connect username in parenthesis allows to get more information - Simon André (simonandre) - Matthias Pigulla (mpdude) - Gabriel Ostrolucký (gadelat) - - Jonathan Wage (jwage) - Vincent Langlet (deviling) + - Jonathan Wage (jwage) - Valentin Udaltsov (vudaltsov) - Grégoire Paris (greg0ire) - Alexandre Salomé (alexandresalome) @@ -69,19 +69,19 @@ The Symfony Connect username in parenthesis allows to get more information - Alexander Mols (asm89) - Gábor Egyed (1ed) - Francis Besset (francisbesset) + - Mathieu Santostefano (welcomattic) - Titouan Galopin (tgalopin) - Pierre du Plessis (pierredup) - David Maicher (dmaicher) - Tomasz Kowalczyk (thunderer) - - Mathieu Santostefano (welcomattic) - Bulat Shakirzyanov (avalanche123) + - Dariusz Ruminski - Iltar van der Berg - Miha Vrhovnik (mvrhov) - Gary PEGEOT (gary-p) - Saša Stamenković (umpirsky) - - Allison Guilhem (a_guilhem) - Alexander Schranz (alexander-schranz) - - Dariusz Ruminski + - Allison Guilhem (a_guilhem) - Mathieu Piot (mpiot) - Vasilij Duško (staff) - Sarah Khalil (saro0h) @@ -102,13 +102,13 @@ The Symfony Connect username in parenthesis allows to get more information - Christian Raue - Eric Clemmons (ericclemmons) - Denis (yethee) + - Alex Pott - Michel Weimerskirch (mweimerskirch) - Issei Murasawa (issei_m) - Arnout Boks (aboks) - Douglas Greenshields (shieldo) - Frank A. Fiebig (fafiebig) - Baldini - - Alex Pott - Fran Moreno (franmomu) - Hubert Lenoir (hubert_lenoir) - Charles Sarrazin (csarrazi) @@ -122,6 +122,7 @@ The Symfony Connect username in parenthesis allows to get more information - Brandon Turner - Massimiliano Arione (garak) - Luis Cordova (cordoval) + - Phil E. Taylor (philetaylor) - Konstantin Myakshin (koc) - Daniel Holmes (dholmes) - Julien Falque (julienfalque) @@ -129,7 +130,6 @@ The Symfony Connect username in parenthesis allows to get more information - Bart van den Burg (burgov) - Vasilij Dusko | CREATION - Jordan Alliot (jalliot) - - Phil E. Taylor (philetaylor) - Théo FIDRY - Joel Wurtz (brouznouf) - John Wards (johnwards) @@ -169,16 +169,18 @@ The Symfony Connect username in parenthesis allows to get more information - Maximilian Beckers (maxbeckers) - Baptiste Clavié (talus) - Alexander Schwenn (xelaris) + - Maxime Helias (maxhelias) - Fabien Pennequin (fabienpennequin) - Dāvis Zālītis (k0d3r1s) - Gordon Franke (gimler) - Malte Schlüter (maltemaltesich) - jeremyFreeAgent (jeremyfreeagent) - Michael Babker (mbabker) + - Alexis Lefebvre + - Christopher Hertel (chertel) - Joshua Thijssen - Vasilij Dusko - Daniel Wehner (dawehner) - - Maxime Helias (maxhelias) - Robert Schönthal (digitalkaoz) - Smaine Milianni (ismail1432) - Hugo Alliaume (kocal) @@ -187,7 +189,6 @@ The Symfony Connect username in parenthesis allows to get more information - noniagriconomie - Eric GELOEN (gelo) - Gabriel Caruso - - Christopher Hertel (chertel) - Stefano Sala (stefano.sala) - Ion Bazan (ionbazan) - Niels Keurentjes (curry684) @@ -195,7 +196,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jhonny Lidfors (jhonne) - Juti Noppornpitak (shiroyuki) - Gregor Harlan (gharlan) - - Alexis Lefebvre - Anthony MARTIN - Sebastian Hörl (blogsh) - Tigran Azatyan (tigranazatyan) @@ -213,10 +213,12 @@ The Symfony Connect username in parenthesis allows to get more information - Andreas Braun - Pablo Godel (pgodel) - Alessandro Chitolina (alekitto) + - Jan Rosier (rosier) - Rafael Dohms (rdohms) - Roman Martinuk (a2a4) - jwdeitch - David Prévot (taffit) + - Florent Morselli (spomky_) - Jérôme Parmentier (lctrs) - Ahmed TAILOULOUTE (ahmedtai) - Simon Berger @@ -236,7 +238,6 @@ The Symfony Connect username in parenthesis allows to get more information - Roland Franssen :) - Romain Monteil (ker0x) - Sergey (upyx) - - Florent Morselli (spomky_) - Marco Pivetta (ocramius) - Antonio Pauletich (x-coder264) - Vincent Touzet (vincenttouzet) @@ -251,7 +252,6 @@ The Symfony Connect username in parenthesis allows to get more information - Oleg Voronkovich - Helmer Aaviksoo - Alessandro Lai (jean85) - - Jan Rosier (rosier) - 77web - Gocha Ossinkine (ossinkine) - Jesse Rushlow (geeshoe) @@ -294,12 +294,14 @@ The Symfony Connect username in parenthesis allows to get more information - Richard Miller - Quynh Xuan Nguyen (seriquynh) - Victor Bocharsky (bocharsky_bw) + - Asis Pattisahusiwa - Aleksandar Jakovljevic (ajakov) - Mario A. Alvarez Garcia (nomack84) - Thomas Rabaix (rande) - D (denderello) - DQNEO - Chi-teck + - Marko Kaznovac (kaznovac) - Andre Rømcke (andrerom) - Bram Leeda (bram123) - Patrick Landolt (scube) @@ -315,17 +317,18 @@ The Symfony Connect username in parenthesis allows to get more information - Mathieu Lemoine (lemoinem) - Christian Schmidt - Andreas Hucks (meandmymonkey) + - Indra Gunawan (indragunawan) - Noel Guilbert (noel) - Bastien Jaillot (bastnic) - Soner Sayakci - Stadly - Stepan Anchugov (kix) - bronze1man + - matlec - sun (sun) - Larry Garfield (crell) - Leo Feyer - Nikolay Labinskiy (e-moe) - - Asis Pattisahusiwa - Martin Schuhfuß (usefulthink) - apetitpa - Guilliam Xavier @@ -356,7 +359,6 @@ The Symfony Connect username in parenthesis allows to get more information - Marcin Sikoń (marphi) - Michele Orselli (orso) - Sven Paulus (subsven) - - Indra Gunawan (indragunawan) - Peter Kruithof (pkruithof) - Alex Hofbauer (alexhofbauer) - Maxime Veber (nek-) @@ -375,8 +377,8 @@ The Symfony Connect username in parenthesis allows to get more information - Jan Sorgalla (jsor) - henrikbjorn - Marcel Beerta (mazen) + - Evert Harmeling (evertharmeling) - Mantis Development - - Marko Kaznovac (kaznovac) - Hidde Wieringa (hiddewie) - dFayet - Rob Frawley 2nd (robfrawley) @@ -399,6 +401,7 @@ The Symfony Connect username in parenthesis allows to get more information - Adam Prager (padam87) - Benoît Burnichon (bburnichon) - maxime.steinhausser + - Iker Ibarguren (ikerib) - Roman Ring (inori) - Xavier Montaña Carreras (xmontana) - Arjen van der Meijden @@ -409,6 +412,7 @@ The Symfony Connect username in parenthesis allows to get more information - Artem Lopata - Patrick McDougle (patrick-mcdougle) - Arnt Gulbrandsen + - Michel Roca (mroca) - Marc Weistroff (futurecat) - Michał (bambucha15) - Danny Berger (dpb587) @@ -433,7 +437,6 @@ The Symfony Connect username in parenthesis allows to get more information - Philipp Cordes (corphi) - Chekote - Thomas Adam - - Evert Harmeling (evertharmeling) - Anderson Müller - jdhoek - Jurica Vlahoviček (vjurica) @@ -457,6 +460,7 @@ The Symfony Connect username in parenthesis allows to get more information - renanbr - Sébastien Lavoie (lavoiesl) - Alex Rock (pierstoval) + - Aurélien Pillevesse (aurelienpillevesse) - Matthieu Lempereur (mryamous) - Wodor Wodorski - Beau Simensen (simensen) @@ -473,7 +477,6 @@ The Symfony Connect username in parenthesis allows to get more information - Pascal Luna (skalpa) - Wouter Van Hecke - Baptiste Lafontaine (magnetik) - - Iker Ibarguren (ikerib) - Michael Hirschler (mvhirsch) - Michael Holm (hollo) - Robert Meijers @@ -511,6 +514,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ismael Ambrosi (iambrosi) - Craig Duncan (duncan3dc) - Emmanuel BORGES + - Karoly Negyesi (chx) - Aurelijus Valeiša (aurelijus) - Jan Decavele (jandc) - Gustavo Piltcher @@ -536,7 +540,6 @@ The Symfony Connect username in parenthesis allows to get more information - Ahmed Raafat - Philippe Segatori - Thibaut Cheymol (tcheymol) - - Aurélien Pillevesse (aurelienpillevesse) - Erin Millard - Matthew Lewinski (lewinski) - Islam Israfilov (islam93) @@ -568,6 +571,7 @@ The Symfony Connect username in parenthesis allows to get more information - mondrake (mondrake) - Yaroslav Kiliba - FORT Pierre-Louis (plfort) + - Jan Böhmer - Terje Bråten - Gonzalo Vilaseca (gonzalovilaseca) - Tarmo Leppänen (tarlepp) @@ -600,11 +604,13 @@ The Symfony Connect username in parenthesis allows to get more information - Kirill chEbba Chebunin - Pol Dellaiera (drupol) - Alex (aik099) + - Kieran Brahney - Fabien Villepinte - SiD (plbsid) - Greg Thornton (xdissent) - Alex Bowers - - Michel Roca (mroca) + - Kev + - kor3k kor3k (kor3k) - Costin Bereveanu (schniper) - Andrii Dembitskyi - Gasan Guseynov (gassan) @@ -634,7 +640,6 @@ The Symfony Connect username in parenthesis allows to get more information - Kai - Alain Hippolyte (aloneh) - Grenier Kévin (mcsky_biig) - - Karoly Negyesi (chx) - Xavier HAUSHERR - Albert Jessurum (ajessu) - Romain Pierre @@ -651,6 +656,7 @@ The Symfony Connect username in parenthesis allows to get more information - Anthon Pang (robocoder) - Julien Galenski (ruian) - Ben Scott (bpscott) + - Shyim - Pablo Lozano (arkadis) - Brian King - quentin neyrat (qneyrat) @@ -663,6 +669,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ahmed Ghanem (ahmedghanem00) - Valentin Jonovs - geoffrey + - Quentin Dequippe (qdequippe) - Benoit Galati (benoitgalati) - Benjamin (yzalis) - Jeanmonod David (jeanmonod) @@ -678,12 +685,14 @@ The Symfony Connect username in parenthesis allows to get more information - Lescot Edouard (idetox) - Dennis Fridrich (dfridrich) - Mohammad Emran Hasan (phpfour) + - Florian Merle (florian-merle) - Dmitriy Mamontov (mamontovdmitriy) - Jan Schumann - Matheo Daninos (mathdns) - Neil Peyssard (nepey) - Niklas Fiekas - Mark Challoner (markchalloner) + - Raffaele Carelle - Andreas Hennings - Markus Bachmann (baachi) - Gunnstein Lye (glye) @@ -697,6 +706,7 @@ The Symfony Connect username in parenthesis allows to get more information - Angelov Dejan (angelov) - Ivan Nikolaev (destillat) - Gildas Quéméner (gquemener) + - Ioan Ovidiu Enache (ionutenache) - Maxim Dovydenok (dovydenok-maxim) - Laurent Masforné (heisenberg) - Claude Khedhiri (ck-developer) @@ -734,10 +744,10 @@ The Symfony Connect username in parenthesis allows to get more information - Vitaliy Tverdokhlib (vitaliytv) - Ariel Ferrandini (aferrandini) - BASAK Semih (itsemih) - - Jan Böhmer - Dirk Pahl (dirkaholic) - Cédric Lombardot (cedriclombardot) - Jérémy REYNAUD (babeuloula) + - Faizan Akram Dar (faizanakram) - Arkadius Stefanski (arkadius) - Jonas Flodén (flojon) - AnneKir @@ -755,6 +765,7 @@ The Symfony Connect username in parenthesis allows to get more information - François Dume (franek) - Jerzy Lekowski (jlekowski) - Raulnet + - Petrisor Ciprian Daniel - Oleksiy (alexndlm) - William Arslett (warslett) - Giso Stallenberg (gisostallenberg) @@ -776,7 +787,6 @@ The Symfony Connect username in parenthesis allows to get more information - Patrick Reimers (preimers) - Brayden Williams (redstar504) - insekticid - - Kieran Brahney - Jérémy M (th3mouk) - Trent Steel (trsteel88) - boombatower @@ -799,7 +809,6 @@ The Symfony Connect username in parenthesis allows to get more information - Matthew Grasmick - Miroslav Šustek (sustmi) - Pablo Díez (pablodip) - - Kev - Kevin McBride - Sergio Santoro - Jonas Elfering @@ -810,7 +819,6 @@ The Symfony Connect username in parenthesis allows to get more information - nikos.sotiropoulos - BENOIT POLASZEK (bpolaszek) - Eduardo Oliveira (entering) - - kor3k kor3k (kor3k) - Oleksii Zhurbytskyi - Bilge - Anatoly Pashin (b1rdex) @@ -827,6 +835,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jérôme Vieilledent (lolautruche) - Roman Anasal - Filip Procházka (fprochazka) + - Sergey Panteleev - Jeroen Thora (bolle) - Markus Lanthaler (lanthaler) - Gigino Chianese (sajito) @@ -845,8 +854,10 @@ The Symfony Connect username in parenthesis allows to get more information - alexpods - Adam Szaraniec - Dariusz Ruminski + - Bahman Mehrdad (bahman) - Pierre Ambroise (dotordu) - Romain Gautier (mykiwi) + - Link1515 - Matthieu Bontemps - Erik Trapman - De Cock Xavier (xdecock) @@ -860,6 +871,7 @@ The Symfony Connect username in parenthesis allows to get more information - Fabrice Bernhard (fabriceb) - Matthijs van den Bos (matthijs) - Markus S. (staabm) + - PatNowak - Bhavinkumar Nakrani (bhavin4u) - Jaik Dean (jaikdean) - Krzysztof Piasecki (krzysztek) @@ -915,7 +927,6 @@ The Symfony Connect username in parenthesis allows to get more information - Markus Staab - Forfarle (forfarle) - Johnny Robeson (johnny) - - Shyim - Disquedur - Benjamin Morel - Guilherme Ferreira @@ -933,7 +944,6 @@ The Symfony Connect username in parenthesis allows to get more information - Pierre-Emmanuel Tanguy (petanguy) - Julien Maulny - Gennadi Janzen - - Quentin Dequippe (qdequippe) - johan Vlaar - Paul Oms - James Hemery @@ -972,7 +982,6 @@ The Symfony Connect username in parenthesis allows to get more information - Ricky Su (ricky) - scyzoryck - Kyle Evans (kevans91) - - Ioan Ovidiu Enache (ionutenache) - Max Rath (drak3) - Cristoforo Cervino (cristoforocervino) - marie @@ -1049,7 +1058,6 @@ The Symfony Connect username in parenthesis allows to get more information - Andy Palmer (andyexeter) - Andrew Neil Forster (krciga22) - Stefan Warman (warmans) - - Faizan Akram Dar (faizanakram) - Tristan Maindron (tmaindron) - Behnoush Norouzali (behnoush) - Marko H. Tamminen (gzumba) @@ -1152,7 +1160,6 @@ The Symfony Connect username in parenthesis allows to get more information - Chris Jones (magikid) - Massimiliano Braglia (massimilianobraglia) - Thijs-jan Veldhuizen (tjveldhuizen) - - Petrisor Ciprian Daniel - Richard Quadling - James Hudson (mrthehud) - Raphaëll Roussel @@ -1250,6 +1257,7 @@ The Symfony Connect username in parenthesis allows to get more information - michaelwilliams - Alexandre Parent - 1emming + - Eric Abouaf (neyric) - Nykopol (nykopol) - Thibault Richard (t-richard) - Jordan Deitch @@ -1268,7 +1276,6 @@ The Symfony Connect username in parenthesis allows to get more information - shubhalgupta - Felds Liscia (felds) - Benjamin Lebon - - Sergey Panteleev - Alexander Grimalovsky (flying) - Andrew Hilobok (hilobok) - Noah Heck (myesain) @@ -1286,6 +1293,7 @@ The Symfony Connect username in parenthesis allows to get more information - izzyp - Jeroen Fiege (fieg) - Martin (meckhardt) + - Wu (wu-agriconomie) - Marcel Hernandez - Evan C - buffcode @@ -1337,7 +1345,6 @@ The Symfony Connect username in parenthesis allows to get more information - Rustam Bakeev (nommyde) - Vincent CHALAMON - Ivan Kurnosov - - Bahman Mehrdad (bahman) - Christopher Hall (mythmakr) - Patrick Dawkins (pjcdawkins) - Paul Kamer (pkamer) @@ -1371,6 +1378,7 @@ The Symfony Connect username in parenthesis allows to get more information - Oriol Viñals - arai - Achilles Kaloeridis (achilles) + - Sébastien Despont (bouillou) - Laurent Bassin (lbassin) - Mouad ZIANI (mouadziani) - Tomasz Ignatiuk @@ -1403,6 +1411,7 @@ The Symfony Connect username in parenthesis allows to get more information - Johnny Peck (johnnypeck) - Jordi Sala Morales (jsala) - Sander De la Marche (sanderdlm) + - skmedix (skmedix) - Loic Chardonnet - Ivan Menshykov - David Romaní @@ -1518,6 +1527,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sébastien Santoro (dereckson) - Daniel Alejandro Castro Arellano (lexcast) - Vincent Chalamon + - Farhad Hedayatifard - Alan ZARLI - Thomas Jarrand - Baptiste Leduc (bleduc) @@ -1581,6 +1591,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jérôme Nadaud (jnadaud) - Frank Naegler - Sam Malone + - Damien Fernandes - Ha Phan (haphan) - Chris Jones (leek) - neghmurken @@ -1650,6 +1661,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ilya Levin (ilyachase) - Hubert Moreau (hmoreau) - Nicolas Appriou + - Silas Joisten (silasjoisten) - Igor Timoshenko (igor.timoshenko) - Pierre-Emmanuel CAPEL - Manuele Menozzi @@ -1665,7 +1677,6 @@ The Symfony Connect username in parenthesis allows to get more information - Nicolas Valverde - Konstantin S. M. Möllers (ksmmoellers) - Ken Stanley - - Raffaele Carelle - ivan - Zachary Tong (polyfractal) - linh @@ -1756,6 +1767,7 @@ The Symfony Connect username in parenthesis allows to get more information - Christophe Meneses (c77men) - Jeremy David (jeremy.david) - Andrei O + - gr8b - Michał Marcin Brzuchalski (brzuchal) - Jordi Rejas - Troy McCabe @@ -1792,8 +1804,10 @@ The Symfony Connect username in parenthesis allows to get more information - Nacho Martin (nacmartin) - Thibaut Chieux - mwos + - Aydin Hassan - Volker Killesreiter (ol0lll) - Vedran Mihočinec (v-m-i) + - Rafał Treffler - Sergey Novikov (s12v) - creiner - Jan Pintr @@ -1983,6 +1997,7 @@ The Symfony Connect username in parenthesis allows to get more information - Eduardo García Sanz (coma) - Arend Hummeling - Makdessi Alex + - Dmitrii Baranov - fduch (fduch) - Juan Miguel Besada Vidal (soutlink) - Takashi Kanemoto (ttskch) @@ -2099,6 +2114,7 @@ The Symfony Connect username in parenthesis allows to get more information - Raphaël Davaillaud - Sander Hagen - cilefen (cilefen) + - Prasetyo Wicaksono (jowy) - Mo Di (modi) - Victor Truhanovich (victor_truhanovich) - Pablo Schläpfer @@ -2150,6 +2166,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jeffrey Cafferata (jcidnl) - Junaid Farooq (junaidfarooq) - Lars Ambrosius Wallenborn (larsborn) + - Pavel Starosek (octisher) - Oriol Mangas Abellan (oriolman) - Sebastian Göttschkes (sgoettschkes) - Marcin Nowak @@ -2159,6 +2176,7 @@ The Symfony Connect username in parenthesis allows to get more information - omniError - Zander Baldwin - László GÖRÖG + - djordy - Kévin Gomez (kevin) - Mihai Nica (redecs) - Andrei Igna @@ -2262,8 +2280,8 @@ The Symfony Connect username in parenthesis allows to get more information - Ilya Chekalsky - Ostrzyciel - George Giannoulopoulos + - Thibault G - Alexander Pasichnik (alex_brizzz) - - Florian Merle (florian-merle) - Felix Eymonot (hyanda) - Luis Ramirez (luisdeimos) - Ilia Sergunin (maranqz) @@ -2293,6 +2311,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ilya Biryukov (ibiryukov) - Mathieu Ledru (matyo91) - Roma (memphys) + - Jozef Môstka (mostkaj) - Florian Caron (shalalalala) - Serhiy Lunak (slunak) - Wojciech Błoszyk (wbloszyk) @@ -2390,6 +2409,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ivan Tse - René Kerner - Nathaniel Catchpole + - Jontsa - Igor Plantaš - upchuk - Adrien Samson (adriensamson) @@ -2408,13 +2428,13 @@ The Symfony Connect username in parenthesis allows to get more information - Wickex - tuqqu - Wojciech Gorczyca + - Ahmad Al-Naib - Neagu Cristian-Doru (cristian-neagu) - Mathieu Morlon (glutamatt) - NIRAV MUKUNDBHAI PATEL (niravpatel919) - Owen Gray (otis) - Rafał Muszyński (rafmus90) - Sébastien Decrême (sebdec) - - Wu (wu-agriconomie) - Timothy Anido (xanido) - Robert-Jan de Dreu - Mara Blaga @@ -2438,6 +2458,7 @@ The Symfony Connect username in parenthesis allows to get more information - Serhii Smirnov - Robert Queck - Peter Bouwdewijn + - Kurt Thiemann - Martins Eglitis - Daniil Gentili - Eduard Morcinek @@ -2446,6 +2467,7 @@ The Symfony Connect username in parenthesis allows to get more information - Matěj Humpál - Kasper Hansen - Nico Hiort af Ornäs + - Eddy - Amine Matmati - Kristen Gilden - caalholm @@ -2495,6 +2517,8 @@ The Symfony Connect username in parenthesis allows to get more information - Thomas Ploch - Victor Prudhomme - Simon Neidhold + - Wouter Ras + - Gil Hadad - Valentin VALCIU - Jeremiah VALERIE - Alexandre Beaujour @@ -2506,11 +2530,13 @@ The Symfony Connect username in parenthesis allows to get more information - Yannick Snobbert - Kevin Dew - James Cowgill + - Žan V. Dragan - sensio - Julien Menth (cfjulien) - Lyubomir Grozdanov (lubo13) - Nicolas Schwartz (nicoschwartz) - Tim Jabs (rubinum) + - Schvoy Norbert (schvoy) - Stéphane Seng (stephaneseng) - Peter Schultz - Robert Korulczyk @@ -2563,6 +2589,7 @@ The Symfony Connect username in parenthesis allows to get more information - Thomas Beaujean - alireza - Michael Bessolov + - sauliusnord - Zdeněk Drahoš - Dan Harper - moldcraft @@ -2608,6 +2635,7 @@ The Symfony Connect username in parenthesis allows to get more information - Tiago Garcia (tiagojsag) - Artiom - Jakub Simon + - TheMhv - Eviljeks - robin.de.croock - Brandon Antonio Lorenzo @@ -2617,12 +2645,14 @@ The Symfony Connect username in parenthesis allows to get more information - Radosław Kowalewski - Enrico Schultz - tpetry + - Nikita Sklyarov - JustDylan23 - Juraj Surman - Martin Eckhardt - natechicago - Victor - Andreas Allacher + - Abdelilah Jabri - Alexis - Leonid Terentyev - Sergei Gorjunov @@ -2715,6 +2745,7 @@ The Symfony Connect username in parenthesis allows to get more information - Andrew Coulton - Ulugbek Miniyarov - Jeremy Benoist + - Antoine Beyet - Michal Gebauer - René Landgrebe - Phil Davis @@ -2912,6 +2943,7 @@ The Symfony Connect username in parenthesis allows to get more information - Artem Lopata (bumz) - Soha Jin - alex + - Alex Niedre - evgkord - Roman Orlov - Simon Ackermann @@ -2937,7 +2969,6 @@ The Symfony Connect username in parenthesis allows to get more information - Julien Moulin (lizjulien) - Raito Akehanareru (raito) - Mauro Foti (skler) - - skmedix (skmedix) - Thibaut Arnoud (thibautarnoud) - Valmont Pehaut-Pietri (valmonzo) - Yannick Warnier (ywarnier) @@ -3008,6 +3039,7 @@ The Symfony Connect username in parenthesis allows to get more information - Cyrille Bourgois (cyrilleb) - Damien Vauchel (damien_vauchel) - Dmitrii Fedorenko (dmifedorenko) + - William Pinaud (docfx) - Frédéric G. Marand (fgm) - Freek Van der Herten (freekmurze) - Luca Genuzio (genuzio) @@ -3174,11 +3206,11 @@ The Symfony Connect username in parenthesis allows to get more information - dakur - florian-michael-mast - tourze + - Dario Guarracino - sam-bee - Vlad Dumitrache - wetternest - Erik van Wingerden - - matlec - Valouleloup - Pathpat - Jaymin G @@ -3230,6 +3262,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dominik Hajduk (dominikalp) - Tomáš Polívka (draczris) - Dennis Smink (dsmink) + - Duncan de Boer (farmer-duck) - Franz Liedke (franzliedke) - Gaylord Poillon (gaylord_p) - gondo (gondo) @@ -3237,6 +3270,7 @@ The Symfony Connect username in parenthesis allows to get more information - Grummfy (grummfy) - Hadrien Cren (hcren) - Gusakov Nikita (hell0w0rd) + - Halil Hakan Karabay (hhkrby) - Oz (import) - Jaap van Otterdijk (jaapio) - Javier Núñez Berrocoso (javiernuber) @@ -3372,6 +3406,7 @@ The Symfony Connect username in parenthesis allows to get more information - Christian Schiffler - Piers Warmers - Sylvain Lorinet + - Pavol Tuka - klyk50 - jc - BenjaminBeck @@ -3454,6 +3489,7 @@ The Symfony Connect username in parenthesis allows to get more information - brian978 - Michael Schneider - n-aleha + - Richard Čepas - Talha Zekeriya Durmuş - Anatol Belski - Javier @@ -3587,6 +3623,8 @@ The Symfony Connect username in parenthesis allows to get more information - Michal Čihař - parhs - Harry Wiseman + - Emilien Escalle + - jwaguet - Diego Campoy - Oncle Tom - Sam Anthony @@ -3649,7 +3687,6 @@ The Symfony Connect username in parenthesis allows to get more information - Bernd Matzner (bmatzner) - Vladimir Vasilev (bobahvas) - Anton (bonio) - - Sébastien Despont (bouillou) - Bram Tweedegolf (bram_tweedegolf) - Brandon Kelly (brandonkelly) - Choong Wei Tjeng (choonge) @@ -3686,6 +3723,7 @@ The Symfony Connect username in parenthesis allows to get more information - Peter Orosz (ill_logical) - Ilia Lazarev (ilzrv) - Imangazaliev Muhammad (imangazaliev) + - wesign (inscrutable01) - Arkadiusz Kondas (itcraftsmanpl) - j0k (j0k) - joris de wit (jdewit) @@ -3759,6 +3797,7 @@ The Symfony Connect username in parenthesis allows to get more information - Julien Sanchez (sumbobyboys) - Ron Gähler (t-ronx) - Guillermo Gisinger (t3chn0r) + - Tomáš Korec (tomkorec) - Tom Newby (tomnewbyau) - Andrew Clark (tqt_andrew_clark) - Aaron Piotrowski (trowski) From 68ca50b472eddc60d181a906077f8374e3496c28 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 29 Jan 2025 08:25:58 +0100 Subject: [PATCH 05/63] Update VERSION for 6.4.18 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 7e357fa528223..e2a01a4ac0835 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -76,12 +76,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.4.18-DEV'; + public const VERSION = '6.4.18'; public const VERSION_ID = 60418; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 4; public const RELEASE_VERSION = 18; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2026'; public const END_OF_LIFE = '11/2027'; From 0549c6f6541b4797fe040333dc99e75818cdf332 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 29 Jan 2025 08:32:49 +0100 Subject: [PATCH 06/63] Bump Symfony version to 6.4.19 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index e2a01a4ac0835..d4ee156b89380 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -76,12 +76,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.4.18'; - public const VERSION_ID = 60418; + public const VERSION = '6.4.19-DEV'; + public const VERSION_ID = 60419; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 18; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 19; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '11/2026'; public const END_OF_LIFE = '11/2027'; From 727ae99526ed907e5abc5e8ee59187c2139b1096 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 29 Jan 2025 08:46:06 +0100 Subject: [PATCH 07/63] Bump Symfony version to 7.2.4 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 4b6ecb68d80d1..d6cd83a21161c 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.2.3'; - public const VERSION_ID = 70203; + public const VERSION = '7.2.4-DEV'; + public const VERSION_ID = 70204; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 2; - public const RELEASE_VERSION = 3; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 4; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '07/2025'; public const END_OF_LIFE = '07/2025'; From 2d37c7908daaa3d3a3faa7271b818dfd2fe7ddca Mon Sep 17 00:00:00 2001 From: Benjamin Ellis Date: Thu, 30 Jan 2025 09:40:15 +0100 Subject: [PATCH 08/63] [Mime] use isRendered method to avoid rendering an email twice --- src/Symfony/Bridge/Twig/Mime/BodyRenderer.php | 2 +- src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php index d5b6d14c139a0..b7ae05f4b0e65 100644 --- a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php +++ b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php @@ -45,7 +45,7 @@ public function render(Message $message): void return; } - if (null === $message->getTextTemplate() && null === $message->getHtmlTemplate()) { + if ($message->isRendered()) { // email has already been rendered return; } diff --git a/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php b/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php index f5d37e7d45c4e..cce8ee9a68839 100644 --- a/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php @@ -105,10 +105,14 @@ public function testRenderedOnce() ; $email->textTemplate('text'); + $this->assertFalse($email->isRendered()); $renderer->render($email); + $this->assertTrue($email->isRendered()); + $this->assertEquals('Text', $email->getTextBody()); $email->text('reset'); + $this->assertTrue($email->isRendered()); $renderer->render($email); $this->assertEquals('reset', $email->getTextBody()); From dc71298643380c20d3c1446bfc7014409cc0b8b0 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 30 Jan 2025 11:03:01 +0100 Subject: [PATCH 09/63] [HttpClient] Fix uploading files > 2GB --- src/Symfony/Component/HttpClient/CurlHttpClient.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 3e15bef74cc9e..15b8d1499fb00 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -237,7 +237,7 @@ public function request(string $method, string $url, array $options = []): Respo if (!\is_string($body)) { if (\is_resource($body)) { - $curlopts[\CURLOPT_INFILE] = $body; + $curlopts[\CURLOPT_READDATA] = $body; } else { $curlopts[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) { static $eof = false; @@ -316,6 +316,9 @@ public function request(string $method, string $url, array $options = []): Respo } foreach ($curlopts as $opt => $value) { + if (\CURLOPT_INFILESIZE === $opt && $value >= 1 << 31) { + $opt = 115; // 115 === CURLOPT_INFILESIZE_LARGE, but it's not defined in PHP + } if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt && (!\defined('CURLOPT_HEADEROPT') || \CURLOPT_HEADEROPT !== $opt)) { $constantName = $this->findConstantName($opt); throw new TransportException(sprintf('Curl option "%s" is not supported.', $constantName ?? $opt)); @@ -472,7 +475,7 @@ private function validateExtraCurlOptions(array $options): void \CURLOPT_RESOLVE => 'resolve', \CURLOPT_NOSIGNAL => 'timeout', \CURLOPT_HTTPHEADER => 'headers', - \CURLOPT_INFILE => 'body', + \CURLOPT_READDATA => 'body', \CURLOPT_READFUNCTION => 'body', \CURLOPT_INFILESIZE => 'body', \CURLOPT_POSTFIELDS => 'body', From f0239d8ce0de01bb5c9ac356881bd0f8754749f6 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 29 Jan 2025 16:01:25 +0100 Subject: [PATCH 10/63] [HttpClient] Fix retrying requests with Psr18Client and NTLM connections --- .../Component/HttpClient/CurlHttpClient.php | 4 ++ .../Component/HttpClient/HttplugClient.php | 39 ++++++++++++++----- .../Component/HttpClient/Psr18Client.php | 39 ++++++++++++++----- 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 67a6c1dddd5d8..ba3191c37c340 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -236,6 +236,10 @@ public function request(string $method, string $url, array $options = []): Respo } if (!\is_string($body)) { + if (isset($options['auth_ntlm'])) { + $curlopts[\CURLOPT_FORBID_REUSE] = true; // Reusing NTLM connections requires seeking capability, which only string bodies support + } + if (\is_resource($body)) { $curlopts[\CURLOPT_INFILE] = $body; } else { diff --git a/src/Symfony/Component/HttpClient/HttplugClient.php b/src/Symfony/Component/HttpClient/HttplugClient.php index 8e1dc1c4cb9e3..e6e4f20d50121 100644 --- a/src/Symfony/Component/HttpClient/HttplugClient.php +++ b/src/Symfony/Component/HttpClient/HttplugClient.php @@ -224,23 +224,42 @@ private function sendPsr7Request(RequestInterface $request, ?bool $buffer = null { try { $body = $request->getBody(); + $headers = $request->getHeaders(); - if ($body->isSeekable()) { - try { - $body->seek(0); - } catch (\RuntimeException) { - // ignore - } + $size = $request->getHeader('content-length')[0] ?? -1; + if (0 > $size && 0 <= $size = $body->getSize() ?? -1) { + $headers['Content-Length'] = [$size]; } - $headers = $request->getHeaders(); - if (!$request->hasHeader('content-length') && 0 <= $size = $body->getSize() ?? -1) { - $headers['Content-Length'] = [$size]; + if (0 <= $size && $size < 1 << 21) { + if ($body->isSeekable()) { + try { + $body->seek(0); + } catch (\RuntimeException) { + // ignore + } + } + + $body = $body->getContents(); + } else { + $body = static function (int $size) use ($body) { + if ($body->isSeekable()) { + try { + $body->seek(0); + } catch (\RuntimeException) { + // ignore + } + } + + while (!$body->eof()) { + yield $body->read($size); + } + }; } $options = [ 'headers' => $headers, - 'body' => static fn (int $size) => $body->read($size), + 'body' => $body, 'buffer' => $buffer, ]; diff --git a/src/Symfony/Component/HttpClient/Psr18Client.php b/src/Symfony/Component/HttpClient/Psr18Client.php index a2a19236e8f67..8bca6f770177b 100644 --- a/src/Symfony/Component/HttpClient/Psr18Client.php +++ b/src/Symfony/Component/HttpClient/Psr18Client.php @@ -88,23 +88,42 @@ public function sendRequest(RequestInterface $request): ResponseInterface { try { $body = $request->getBody(); + $headers = $request->getHeaders(); - if ($body->isSeekable()) { - try { - $body->seek(0); - } catch (\RuntimeException) { - // ignore - } + $size = $request->getHeader('content-length')[0] ?? -1; + if (0 > $size && 0 <= $size = $body->getSize() ?? -1) { + $headers['Content-Length'] = [$size]; } - $headers = $request->getHeaders(); - if (!$request->hasHeader('content-length') && 0 <= $size = $body->getSize() ?? -1) { - $headers['Content-Length'] = [$size]; + if (0 <= $size && $size < 1 << 21) { + if ($body->isSeekable()) { + try { + $body->seek(0); + } catch (\RuntimeException) { + // ignore + } + } + + $body = $body->getContents(); + } else { + $body = static function (int $size) use ($body) { + if ($body->isSeekable()) { + try { + $body->seek(0); + } catch (\RuntimeException) { + // ignore + } + } + + while (!$body->eof()) { + yield $body->read($size); + } + }; } $options = [ 'headers' => $headers, - 'body' => static fn (int $size) => $body->read($size), + 'body' => $body, ]; if ('1.0' === $request->getProtocolVersion()) { From f8fe846d0904c3af3aaf0758aa60a4f6a44619e0 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 2 Feb 2025 20:54:34 +0100 Subject: [PATCH 11/63] relax expected format for PHP 8.5 compatibility --- .../ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt index 81becafd8e350..80a7645770a62 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt @@ -40,7 +40,7 @@ object(Symfony\Component\ErrorHandler\Error\FatalError)#%d (%d) { string(209) "Error: Class Symfony\Component\ErrorHandler\Broken contains 5 abstract methods and must therefore be declared abstract or implement the remaining methods (Iterator::current, Iterator::next, Iterator::key, ...)" %a ["error":"Symfony\Component\ErrorHandler\Error\FatalError":private]=> - array(4) { + array(%d) { ["type"]=> int(1) ["message"]=> @@ -48,6 +48,6 @@ object(Symfony\Component\ErrorHandler\Error\FatalError)#%d (%d) { ["file"]=> string(%d) "%s" ["line"]=> - int(%d) + int(%d)%A } } From 31ef3e2f4004792e162734a46d9c025c7e6a49ba Mon Sep 17 00:00:00 2001 From: Juliano Petronetto Date: Mon, 3 Feb 2025 15:30:57 +0100 Subject: [PATCH 12/63] [Validator] Review missing translations for Portuguese --- .../Resources/translations/validators.pt.xlf | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf index 759eb5369bd8e..68a7f5ff6c7ea 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf @@ -444,31 +444,31 @@ This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. - Este valor é muito curto. Deve conter pelo menos uma palavra.|Este valor é muito curto. Deve conter pelo menos {{ min }} palavras. + Este valor é muito curto. Deve conter pelo menos uma palavra.|Este valor é muito curto. Deve conter pelo menos {{ min }} palavras. This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. - Este valor é muito longo. Deve conter apenas uma palavra.|Este valor é muito longo. Deve conter {{ max }} palavras ou menos. + Este valor é muito longo. Deve conter apenas uma palavra.|Este valor é muito longo. Deve conter {{ max }} palavras ou menos. This value does not represent a valid week in the ISO 8601 format. - Este valor não representa uma semana válida no formato ISO 8601. + Este valor não representa uma semana válida no formato ISO 8601. This value is not a valid week. - Este valor não é uma semana válida. + Este valor não é uma semana válida. This value should not be before week "{{ min }}". - Este valor não deve ser anterior à semana "{{ min }}". + Este valor não deve ser anterior à semana "{{ min }}". This value should not be after week "{{ max }}". - Este valor não deve estar após a semana "{{ max }}". + Este valor não deve estar após a semana "{{ max }}". This value is not a valid slug. - Este valor não é um slug válido. + Este valor não é um slug válido. From 90dc4ee8139df1b10b379f87dd72da7af250dfac Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Mon, 3 Feb 2025 17:05:54 +0100 Subject: [PATCH 13/63] [TypeInfo] Fix promoted property phpdoc reading --- .../Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php | 8 ++++++++ .../PhpDocAwareReflectionTypeResolverTest.php | 1 + .../TypeResolver/PhpDocAwareReflectionTypeResolver.php | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php index 479ccfa2afc01..30141f95c3d0f 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php @@ -9,6 +9,14 @@ final class DummyWithPhpDoc */ public mixed $arrayOfDummies = []; + /** + * @param bool $promoted + */ + public function __construct( + public mixed $promoted, + ) { + } + /** * @param Dummy $dummy * diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php index 261fd19f18e96..7e92638a9ce38 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php @@ -28,6 +28,7 @@ public function testReadPhpDoc() $reflection = new \ReflectionClass(DummyWithPhpDoc::class); $this->assertEquals(Type::array(Type::object(Dummy::class)), $resolver->resolve($reflection->getProperty('arrayOfDummies'))); + $this->assertEquals(Type::bool(), $resolver->resolve($reflection->getProperty('promoted'))); $this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy'))); $this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy')->getParameters()[0])); } diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php index 1037b4828f144..9f71ee4bc2ed8 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php @@ -65,7 +65,7 @@ public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type } $docComment = match (true) { - $subject instanceof \ReflectionProperty => $subject->getDocComment(), + $subject instanceof \ReflectionProperty => $subject->isPromoted() ? $subject->getDeclaringClass()?->getConstructor()?->getDocComment() : $subject->getDocComment(), $subject instanceof \ReflectionParameter => $subject->getDeclaringFunction()->getDocComment(), $subject instanceof \ReflectionFunctionAbstract => $subject->getDocComment(), }; @@ -77,7 +77,7 @@ public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type $typeContext ??= $this->typeContextFactory->createFromReflection($subject); $tagName = match (true) { - $subject instanceof \ReflectionProperty => '@var', + $subject instanceof \ReflectionProperty => $subject->isPromoted() ? '@param' : '@var', $subject instanceof \ReflectionParameter => '@param', $subject instanceof \ReflectionFunctionAbstract => '@return', }; From b8dd84205a94f756428118f1940a7056a5c3a360 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 4 Feb 2025 09:22:03 +0100 Subject: [PATCH 14/63] [Security] Fix typo in deprecation message --- .../Security/Http/Authenticator/RememberMeAuthenticator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php index d71b417788724..c695be084861b 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -60,7 +60,7 @@ public function __construct( LoggerInterface|string|null $logger = null, ) { if (\is_string($tokenStorage)) { - trigger_deprecation('symfony/security-core', '7.2', 'The "$secret" argument of "%s()" is deprecated.', __METHOD__); + trigger_deprecation('symfony/security-http', '7.2', 'The "$secret" argument of "%s()" is deprecated.', __METHOD__); $this->secret = $tokenStorage; $tokenStorage = $cookieName; From 19a50f7e5fb4e429a59143f24ecbcbc3186168a5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 3 Feb 2025 10:59:13 +0100 Subject: [PATCH 15/63] [HttpClient] Fix buffering AsyncResponse with no passthru --- .../HttpClient/Response/AsyncResponse.php | 17 +++++------------ .../Tests/AsyncDecoratorTraitTest.php | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/HttpClient/Response/AsyncResponse.php b/src/Symfony/Component/HttpClient/Response/AsyncResponse.php index 25f6409b6e319..7aa16bcb17c00 100644 --- a/src/Symfony/Component/HttpClient/Response/AsyncResponse.php +++ b/src/Symfony/Component/HttpClient/Response/AsyncResponse.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpClient\Response; use Symfony\Component\HttpClient\Chunk\ErrorChunk; -use Symfony\Component\HttpClient\Chunk\FirstChunk; use Symfony\Component\HttpClient\Chunk\LastChunk; use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Contracts\HttpClient\ChunkInterface; @@ -245,7 +244,7 @@ public static function stream(iterable $responses, ?float $timeout = null, ?stri $wrappedResponses[] = $r->response; if ($r->stream) { - yield from self::passthruStream($response = $r->response, $r, new FirstChunk(), $asyncMap); + yield from self::passthruStream($response = $r->response, $r, $asyncMap, new LastChunk()); if (!isset($asyncMap[$response])) { array_pop($wrappedResponses); @@ -276,15 +275,9 @@ public static function stream(iterable $responses, ?float $timeout = null, ?stri } if (!$r->passthru) { - if (null !== $chunk->getError() || $chunk->isLast()) { - unset($asyncMap[$response]); - } elseif (null !== $r->content && '' !== ($content = $chunk->getContent()) && \strlen($content) !== fwrite($r->content, $content)) { - $chunk = new ErrorChunk($r->offset, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($content)))); - $r->info['error'] = $chunk->getError(); - $r->response->cancel(); - } + $r->stream = (static fn () => yield $chunk)(); + yield from self::passthruStream($response, $r, $asyncMap); - yield $r => $chunk; continue; } @@ -347,13 +340,13 @@ private static function passthru(HttpClientInterface $client, self $r, ChunkInte } $r->stream = $stream; - yield from self::passthruStream($response, $r, null, $asyncMap); + yield from self::passthruStream($response, $r, $asyncMap); } /** * @param \SplObjectStorage|null $asyncMap */ - private static function passthruStream(ResponseInterface $response, self $r, ?ChunkInterface $chunk, ?\SplObjectStorage $asyncMap): \Generator + private static function passthruStream(ResponseInterface $response, self $r, ?\SplObjectStorage $asyncMap, ?ChunkInterface $chunk = null): \Generator { while (true) { try { diff --git a/src/Symfony/Component/HttpClient/Tests/AsyncDecoratorTraitTest.php b/src/Symfony/Component/HttpClient/Tests/AsyncDecoratorTraitTest.php index 97e4c42a0c79a..8e8b43348b601 100644 --- a/src/Symfony/Component/HttpClient/Tests/AsyncDecoratorTraitTest.php +++ b/src/Symfony/Component/HttpClient/Tests/AsyncDecoratorTraitTest.php @@ -232,6 +232,20 @@ public function testBufferPurePassthru() $this->assertStringContainsString('SERVER_PROTOCOL', $response->getContent()); $this->assertStringContainsString('HTTP_HOST', $response->getContent()); + + $client = new class(parent::getHttpClient(__FUNCTION__)) implements HttpClientInterface { + use AsyncDecoratorTrait; + + public function request(string $method, string $url, array $options = []): ResponseInterface + { + return new AsyncResponse($this->client, $method, $url, $options); + } + }; + + $response = $client->request('GET', 'http://localhost:8057/'); + + $this->assertStringContainsString('SERVER_PROTOCOL', $response->getContent()); + $this->assertStringContainsString('HTTP_HOST', $response->getContent()); } public function testRetryTimeout() From b20892fb1e0ed9084928d0e4b143efaa69d4dfff Mon Sep 17 00:00:00 2001 From: HypeMC Date: Thu, 30 Jan 2025 23:42:50 +0100 Subject: [PATCH 16/63] [Lock] Fix Predis error handling --- .../Component/Lock/Store/RedisStore.php | 42 ++++++++++++++----- .../Store/PredisStoreWithExceptionsTest.php | 36 ++++++++++++++++ ...p => PredisStoreWithoutExceptionsTest.php} | 2 +- 3 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 src/Symfony/Component/Lock/Tests/Store/PredisStoreWithExceptionsTest.php rename src/Symfony/Component/Lock/Tests/Store/{PredisStoreTest.php => PredisStoreWithoutExceptionsTest.php} (93%) diff --git a/src/Symfony/Component/Lock/Store/RedisStore.php b/src/Symfony/Component/Lock/Store/RedisStore.php index 5d828449131a4..f2d8a5e9766fb 100644 --- a/src/Symfony/Component/Lock/Store/RedisStore.php +++ b/src/Symfony/Component/Lock/Store/RedisStore.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Lock\Store; use Predis\Response\Error; +use Predis\Response\ServerException; use Relay\Relay; use Symfony\Component\Lock\Exception\InvalidTtlException; use Symfony\Component\Lock\Exception\LockConflictedException; @@ -284,21 +285,18 @@ private function evaluate(string $script, string $resource, array $args): mixed \assert($this->redis instanceof \Predis\ClientInterface); - $result = $this->redis->evalSha($scriptSha, 1, $resource, ...$args); - if ($result instanceof Error && str_starts_with($result->getMessage(), self::NO_SCRIPT_ERROR_MESSAGE_PREFIX)) { - $result = $this->redis->script('LOAD', $script); - if ($result instanceof Error) { - throw new LockStorageException($result->getMessage()); + try { + return $this->handlePredisError(fn () => $this->redis->evalSha($scriptSha, 1, $resource, ...$args)); + } catch (LockStorageException $e) { + // Fallthrough only if we need to load the script + if (!str_starts_with($e->getMessage(), self::NO_SCRIPT_ERROR_MESSAGE_PREFIX)) { + throw $e; } - - $result = $this->redis->evalSha($scriptSha, 1, $resource, ...$args); } - if ($result instanceof Error) { - throw new LockStorageException($result->getMessage()); - } + $this->handlePredisError(fn () => $this->redis->script('LOAD', $script)); - return $result; + return $this->handlePredisError(fn () => $this->redis->evalSha($scriptSha, 1, $resource, ...$args)); } private function getUniqueToken(Key $key): string @@ -347,4 +345,26 @@ private function getNowCode(): string now = math.floor(now * 1000) '; } + + /** + * @template T + * + * @param callable(): T $callback + * + * @return T + */ + private function handlePredisError(callable $callback): mixed + { + try { + $result = $callback(); + } catch (ServerException $e) { + throw new LockStorageException($e->getMessage(), $e->getCode(), $e); + } + + if ($result instanceof Error) { + throw new LockStorageException($result->getMessage()); + } + + return $result; + } } diff --git a/src/Symfony/Component/Lock/Tests/Store/PredisStoreWithExceptionsTest.php b/src/Symfony/Component/Lock/Tests/Store/PredisStoreWithExceptionsTest.php new file mode 100644 index 0000000000000..6b24711b89a8e --- /dev/null +++ b/src/Symfony/Component/Lock/Tests/Store/PredisStoreWithExceptionsTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Lock\Tests\Store; + +/** + * @group integration + */ +class PredisStoreWithExceptionsTest extends AbstractRedisStoreTestCase +{ + public static function setUpBeforeClass(): void + { + $redis = new \Predis\Client(array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => null])); + try { + $redis->connect(); + } catch (\Exception $e) { + self::markTestSkipped($e->getMessage()); + } + } + + protected function getRedisConnection(): \Predis\Client + { + $redis = new \Predis\Client(array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => null])); + $redis->connect(); + + return $redis; + } +} diff --git a/src/Symfony/Component/Lock/Tests/Store/PredisStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/PredisStoreWithoutExceptionsTest.php similarity index 93% rename from src/Symfony/Component/Lock/Tests/Store/PredisStoreTest.php rename to src/Symfony/Component/Lock/Tests/Store/PredisStoreWithoutExceptionsTest.php index 74a72b5a4003a..bb135a4676406 100644 --- a/src/Symfony/Component/Lock/Tests/Store/PredisStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/PredisStoreWithoutExceptionsTest.php @@ -16,7 +16,7 @@ * * @group integration */ -class PredisStoreTest extends AbstractRedisStoreTestCase +class PredisStoreWithoutExceptionsTest extends AbstractRedisStoreTestCase { public static function setUpBeforeClass(): void { From b647cdf329619449a126d8c6d8ca8cb62c2f5122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Tue, 4 Feb 2025 11:02:47 +0100 Subject: [PATCH 17/63] Prevent empty request body stream in HttplugClient and Psr18Client This prevents adding `Content-Length` and `Transfer-Encoding` headers when sending a request with an empty body using CurlHttpClient --- src/Symfony/Component/HttpClient/HttplugClient.php | 6 ++++-- src/Symfony/Component/HttpClient/Psr18Client.php | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpClient/HttplugClient.php b/src/Symfony/Component/HttpClient/HttplugClient.php index e6e4f20d50121..dad01dcc89c8e 100644 --- a/src/Symfony/Component/HttpClient/HttplugClient.php +++ b/src/Symfony/Component/HttpClient/HttplugClient.php @@ -227,11 +227,13 @@ private function sendPsr7Request(RequestInterface $request, ?bool $buffer = null $headers = $request->getHeaders(); $size = $request->getHeader('content-length')[0] ?? -1; - if (0 > $size && 0 <= $size = $body->getSize() ?? -1) { + if (0 > $size && 0 < $size = $body->getSize() ?? -1) { $headers['Content-Length'] = [$size]; } - if (0 <= $size && $size < 1 << 21) { + if (0 === $size) { + $body = ''; + } elseif (0 < $size && $size < 1 << 21) { if ($body->isSeekable()) { try { $body->seek(0); diff --git a/src/Symfony/Component/HttpClient/Psr18Client.php b/src/Symfony/Component/HttpClient/Psr18Client.php index 8bca6f770177b..5ab4a8d3ce41d 100644 --- a/src/Symfony/Component/HttpClient/Psr18Client.php +++ b/src/Symfony/Component/HttpClient/Psr18Client.php @@ -91,11 +91,13 @@ public function sendRequest(RequestInterface $request): ResponseInterface $headers = $request->getHeaders(); $size = $request->getHeader('content-length')[0] ?? -1; - if (0 > $size && 0 <= $size = $body->getSize() ?? -1) { + if (0 > $size && 0 < $size = $body->getSize() ?? -1) { $headers['Content-Length'] = [$size]; } - if (0 <= $size && $size < 1 << 21) { + if (0 === $size) { + $body = ''; + } elseif (0 < $size && $size < 1 << 21) { if ($body->isSeekable()) { try { $body->seek(0); From c237f8e0f34a7bd20fe95267bf7144e7f39e6f12 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 4 Feb 2025 11:18:48 +0100 Subject: [PATCH 18/63] [Process] Fix process status tracking --- src/Symfony/Component/Process/Process.php | 18 +++--------------- .../Component/Process/Tests/ProcessTest.php | 3 +++ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 280a732d5db54..6dfb25e7b9c5d 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -80,7 +80,6 @@ class Process implements \IteratorAggregate private WindowsPipes|UnixPipes $processPipes; private ?int $latestSignal = null; - private ?int $cachedExitCode = null; private static ?bool $sigchild = null; @@ -1289,21 +1288,10 @@ protected function updateStatus(bool $blocking) return; } - $this->processInformation = proc_get_status($this->process); - $running = $this->processInformation['running']; - - // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call. - // Subsequent calls return -1 as the process is discarded. This workaround caches the first - // retrieved exit status for consistent results in later calls, mimicking PHP 8.3 behavior. - if (\PHP_VERSION_ID < 80300) { - if (!isset($this->cachedExitCode) && !$running && -1 !== $this->processInformation['exitcode']) { - $this->cachedExitCode = $this->processInformation['exitcode']; - } - - if (isset($this->cachedExitCode) && !$running && -1 === $this->processInformation['exitcode']) { - $this->processInformation['exitcode'] = $this->cachedExitCode; - } + if ($this->processInformation['running'] ?? true) { + $this->processInformation = proc_get_status($this->process); } + $running = $this->processInformation['running']; $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index eb0b2bcc3c3ea..0f302c2aabd3c 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -711,6 +711,9 @@ public function testProcessIsSignaledIfStopped() if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Windows does not support POSIX signals'); } + if (\PHP_VERSION_ID < 80300 && isset($_SERVER['GITHUB_ACTIONS'])) { + $this->markTestSkipped('Transient on GHA with PHP < 8.3'); + } $process = $this->getProcessForCode('sleep(32);'); $process->start(); From 018a384828be7e84ec1826889108d9ff88eaf056 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 4 Feb 2025 17:23:00 +0100 Subject: [PATCH 19/63] pass CURLOPT_INFILESIZE_LARGE only when supported --- src/Symfony/Component/HttpClient/CurlHttpClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 15b8d1499fb00..31eca9d836f21 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -316,8 +316,8 @@ public function request(string $method, string $url, array $options = []): Respo } foreach ($curlopts as $opt => $value) { - if (\CURLOPT_INFILESIZE === $opt && $value >= 1 << 31) { - $opt = 115; // 115 === CURLOPT_INFILESIZE_LARGE, but it's not defined in PHP + if (\PHP_INT_SIZE === 8 && \defined('CURLOPT_INFILESIZE_LARGE') && \CURLOPT_INFILESIZE === $opt && $value >= 1 << 31) { + $opt = \CURLOPT_INFILESIZE_LARGE; } if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt && (!\defined('CURLOPT_HEADEROPT') || \CURLOPT_HEADEROPT !== $opt)) { $constantName = $this->findConstantName($opt); From 8cb8f553ff0a6edc8a18c1761e929f15fa33879e Mon Sep 17 00:00:00 2001 From: HypeMC Date: Wed, 5 Feb 2025 08:18:49 +0100 Subject: [PATCH 20/63] [Serializer] Handle default context in named Serializer --- .../Serializer/DependencyInjection/SerializerPass.php | 2 +- .../Tests/DependencyInjection/SerializerPassTest.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php index bf1296e9384b3..7b7f6f1c2313b 100644 --- a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php +++ b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php @@ -151,7 +151,7 @@ private function configureNamedSerializers(ContainerBuilder $container): void $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $config['default_context']); - $container->registerChild($serializerId, 'serializer'); + $container->registerChild($serializerId, 'serializer')->setArgument('$defaultContext', $config['default_context']); $container->registerAliasForArgument($serializerId, SerializerInterface::class, $serializerName.'.serializer'); $this->configureSerializer($container, $serializerId, $normalizers, $encoders, $serializerName); diff --git a/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php b/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php index ca54460a72565..769243be25f88 100644 --- a/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php +++ b/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php @@ -556,7 +556,7 @@ public function testBindSerializerDefaultContextToNamedSerializers() 'api' => ['default_context' => $defaultContext = ['enable_max_depth' => true]], ]); - $container->register('serializer')->setArguments([null, null]); + $container->register('serializer')->setArguments([null, null, []]); $definition = $container->register('n1') ->addTag('serializer.normalizer', ['serializer' => '*']) ->addTag('serializer.encoder', ['serializer' => '*']) @@ -570,6 +570,8 @@ public function testBindSerializerDefaultContextToNamedSerializers() $bindings = $container->getDefinition('n1.api')->getBindings(); $this->assertArrayHasKey('array $defaultContext', $bindings); $this->assertEquals($bindings['array $defaultContext'], new BoundArgument($defaultContext, false)); + $this->assertArrayNotHasKey('$defaultContext', $container->getDefinition('serializer')->getArguments()); + $this->assertEquals($defaultContext, $container->getDefinition('serializer.api')->getArgument('$defaultContext')); } public function testNamedSerializersAreRegistered() From 981236c547e76604241222f7b335756b98dba0e8 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 5 Feb 2025 09:21:03 +0100 Subject: [PATCH 21/63] skip transient test on GitHub Actions --- src/Symfony/Component/Process/Tests/ProcessTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index 06b2e803829c4..8f9f131d357ed 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -710,6 +710,9 @@ public function testProcessIsNotSignaled() if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Windows does not support POSIX signals'); } + if (\PHP_VERSION_ID < 80300 && isset($_SERVER['GITHUB_ACTIONS'])) { + $this->markTestSkipped('Transient on GHA with PHP < 8.3'); + } $process = $this->getProcess('echo foo'); $process->run(); From fc8ee6ac0ad6a67b1dd7a7223a7f956572e1e587 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 5 Feb 2025 09:21:03 +0100 Subject: [PATCH 22/63] skip transient test on GitHub Actions --- src/Symfony/Component/Process/Tests/ProcessTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index 8f9f131d357ed..b17bfc7a31aa0 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -710,9 +710,6 @@ public function testProcessIsNotSignaled() if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Windows does not support POSIX signals'); } - if (\PHP_VERSION_ID < 80300 && isset($_SERVER['GITHUB_ACTIONS'])) { - $this->markTestSkipped('Transient on GHA with PHP < 8.3'); - } $process = $this->getProcess('echo foo'); $process->run(); @@ -1689,6 +1686,9 @@ public function testNotIgnoringSignal() if (!\function_exists('pcntl_signal')) { $this->markTestSkipped('pnctl extension is required.'); } + if (\PHP_VERSION_ID < 80300 && isset($_SERVER['GITHUB_ACTIONS'])) { + $this->markTestSkipped('Transient on GHA with PHP < 8.3'); + } $process = $this->getProcess(['sleep', '10']); From ee3451b3c7a523d8d088164277e395778bd5e653 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 7 Feb 2025 15:14:19 +0100 Subject: [PATCH 23/63] [HttpClient] Fix activity tracking leading to negative timeout errors --- .../HttpClient/Response/AmpResponse.php | 20 +++++------ .../HttpClient/Response/CurlResponse.php | 4 +-- .../HttpClient/Response/MockResponse.php | 2 +- .../HttpClient/Response/NativeResponse.php | 2 +- .../Response/TransportResponseTrait.php | 36 ++++++++++--------- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/Symfony/Component/HttpClient/Response/AmpResponse.php b/src/Symfony/Component/HttpClient/Response/AmpResponse.php index e01d97eb868e4..00001ccecba06 100644 --- a/src/Symfony/Component/HttpClient/Response/AmpResponse.php +++ b/src/Symfony/Component/HttpClient/Response/AmpResponse.php @@ -179,19 +179,17 @@ private static function schedule(self $response, array &$runningResponses): void /** * @param AmpClientState $multi */ - private static function perform(ClientState $multi, ?array &$responses = null): void + private static function perform(ClientState $multi, ?array $responses = null): void { - if ($responses) { - foreach ($responses as $response) { - try { - if ($response->info['start_time']) { - $response->info['total_time'] = microtime(true) - $response->info['start_time']; - ($response->onProgress)(); - } - } catch (\Throwable $e) { - $multi->handlesActivity[$response->id][] = null; - $multi->handlesActivity[$response->id][] = $e; + foreach ($responses ?? [] as $response) { + try { + if ($response->info['start_time']) { + $response->info['total_time'] = microtime(true) - $response->info['start_time']; + ($response->onProgress)(); } + } catch (\Throwable $e) { + $multi->handlesActivity[$response->id][] = null; + $multi->handlesActivity[$response->id][] = $e; } } } diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index 88cb764384dad..e5dfd3e52d3c1 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -265,11 +265,11 @@ private static function schedule(self $response, array &$runningResponses): void /** * @param CurlClientState $multi */ - private static function perform(ClientState $multi, ?array &$responses = null): void + private static function perform(ClientState $multi, ?array $responses = null): void { if ($multi->performing) { if ($responses) { - $response = current($responses); + $response = $responses[array_key_first($responses)]; $multi->handlesActivity[(int) $response->handle][] = null; $multi->handlesActivity[(int) $response->handle][] = new TransportException(sprintf('Userland callback cannot use the client nor the response while processing "%s".', curl_getinfo($response->handle, \CURLINFO_EFFECTIVE_URL))); } diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index 0493bcb7c6fc2..c6b96c0b18416 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -167,7 +167,7 @@ protected static function schedule(self $response, array &$runningResponses): vo $runningResponses[0][1][$response->id] = $response; } - protected static function perform(ClientState $multi, array &$responses): void + protected static function perform(ClientState $multi, array $responses): void { foreach ($responses as $response) { $id = $response->id; diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php index 9a5184ed6f6d4..54312884cd957 100644 --- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php +++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php @@ -228,7 +228,7 @@ private static function schedule(self $response, array &$runningResponses): void /** * @param NativeClientState $multi */ - private static function perform(ClientState $multi, ?array &$responses = null): void + private static function perform(ClientState $multi, ?array $responses = null): void { foreach ($multi->openHandles as $i => [$pauseExpiry, $h, $buffer, $onProgress]) { if ($pauseExpiry) { diff --git a/src/Symfony/Component/HttpClient/Response/TransportResponseTrait.php b/src/Symfony/Component/HttpClient/Response/TransportResponseTrait.php index 7b65fd7990464..95f7e9624c912 100644 --- a/src/Symfony/Component/HttpClient/Response/TransportResponseTrait.php +++ b/src/Symfony/Component/HttpClient/Response/TransportResponseTrait.php @@ -92,7 +92,7 @@ abstract protected static function schedule(self $response, array &$runningRespo /** * Performs all pending non-blocking operations. */ - abstract protected static function perform(ClientState $multi, array &$responses): void; + abstract protected static function perform(ClientState $multi, array $responses): void; /** * Waits for network activity. @@ -150,10 +150,15 @@ public static function stream(iterable $responses, ?float $timeout = null): \Gen $lastActivity = hrtime(true) / 1E9; $elapsedTimeout = 0; - if ($fromLastTimeout = 0.0 === $timeout && '-0' === (string) $timeout) { - $timeout = null; - } elseif ($fromLastTimeout = 0 > $timeout) { - $timeout = -$timeout; + if ((0.0 === $timeout && '-0' === (string) $timeout) || 0 > $timeout) { + $timeout = $timeout ? -$timeout : null; + + /** @var ClientState $multi */ + foreach ($runningResponses as [$multi]) { + if (null !== $multi->lastTimeout) { + $elapsedTimeout = max($elapsedTimeout, $lastActivity - $multi->lastTimeout); + } + } } while (true) { @@ -162,8 +167,7 @@ public static function stream(iterable $responses, ?float $timeout = null): \Gen $timeoutMin = $timeout ?? \INF; /** @var ClientState $multi */ - foreach ($runningResponses as $i => [$multi]) { - $responses = &$runningResponses[$i][1]; + foreach ($runningResponses as $i => [$multi, &$responses]) { self::perform($multi, $responses); foreach ($responses as $j => $response) { @@ -171,26 +175,25 @@ public static function stream(iterable $responses, ?float $timeout = null): \Gen $timeoutMin = min($timeoutMin, $response->timeout, 1); $chunk = false; - if ($fromLastTimeout && null !== $multi->lastTimeout) { - $elapsedTimeout = hrtime(true) / 1E9 - $multi->lastTimeout; - } - if (isset($multi->handlesActivity[$j])) { $multi->lastTimeout = null; + $elapsedTimeout = 0; } elseif (!isset($multi->openHandles[$j])) { + $hasActivity = true; unset($responses[$j]); continue; } elseif ($elapsedTimeout >= $timeoutMax) { $multi->handlesActivity[$j] = [new ErrorChunk($response->offset, sprintf('Idle timeout reached for "%s".', $response->getInfo('url')))]; $multi->lastTimeout ??= $lastActivity; + $elapsedTimeout = $timeoutMax; } else { continue; } - while ($multi->handlesActivity[$j] ?? false) { - $hasActivity = true; - $elapsedTimeout = 0; + $lastActivity = null; + $hasActivity = true; + while ($multi->handlesActivity[$j] ?? false) { if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) { if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate, $chunk)) { $multi->handlesActivity[$j] = [null, new TransportException(sprintf('Error while processing content unencoding for "%s".', $response->getInfo('url')))]; @@ -227,7 +230,6 @@ public static function stream(iterable $responses, ?float $timeout = null): \Gen } } elseif ($chunk instanceof ErrorChunk) { unset($responses[$j]); - $elapsedTimeout = $timeoutMax; } elseif ($chunk instanceof FirstChunk) { if ($response->logger) { $info = $response->getInfo(); @@ -274,10 +276,12 @@ public static function stream(iterable $responses, ?float $timeout = null): \Gen if ($chunk instanceof ErrorChunk && !$chunk->didThrow()) { // Ensure transport exceptions are always thrown $chunk->getContent(); + throw new \LogicException('A transport exception should have been thrown.'); } } if (!$responses) { + $hasActivity = true; unset($runningResponses[$i]); } @@ -291,7 +295,7 @@ public static function stream(iterable $responses, ?float $timeout = null): \Gen } if ($hasActivity) { - $lastActivity = hrtime(true) / 1E9; + $lastActivity ??= hrtime(true) / 1E9; continue; } From bf1e312250df72d7a68e1caed7ecfaff75045700 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 7 Feb 2025 19:13:17 +0100 Subject: [PATCH 24/63] [Form][FrameworkBundle] Use auto-configuration to make the default CSRF token id apply only to the app; not to bundles --- .../FrameworkExtension.php | 6 ++---- .../Resources/config/form_csrf.php | 2 +- .../Form/DependencyInjection/FormPass.php | 13 ++++++++++++ .../Csrf/Type/FormTypeCsrfExtension.php | 16 +++++++++++---- .../DependencyInjection/FormPassTest.php | 20 +++++++++++++++++++ 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 556a0cff6aad7..98dd074f4f7b2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -615,7 +615,7 @@ public function load(array $configs, ContainerBuilder $container): void $container->registerForAutoconfiguration(DataCollectorInterface::class) ->addTag('data_collector'); $container->registerForAutoconfiguration(FormTypeInterface::class) - ->addTag('form.type'); + ->addTag('form.type', ['csrf_token_id' => '%.form.type_extension.csrf.token_id%']); $container->registerForAutoconfiguration(FormTypeGuesserInterface::class) ->addTag('form.type_guesser'); $container->registerForAutoconfiguration(FormTypeExtensionInterface::class) @@ -777,9 +777,7 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont $container->setParameter('form.type_extension.csrf.enabled', true); $container->setParameter('form.type_extension.csrf.field_name', $config['form']['csrf_protection']['field_name']); $container->setParameter('form.type_extension.csrf.field_attr', $config['form']['csrf_protection']['field_attr']); - - $container->getDefinition('form.type_extension.csrf') - ->replaceArgument(7, $config['form']['csrf_protection']['token_id']); + $container->setParameter('.form.type_extension.csrf.token_id', $config['form']['csrf_protection']['token_id']); } else { $container->setParameter('form.type_extension.csrf.enabled', false); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php index c63d087c864db..a86bb7c60fdcf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php @@ -24,7 +24,7 @@ param('validator.translation_domain'), service('form.server_params'), param('form.type_extension.csrf.field_attr'), - abstract_arg('framework.form.csrf_protection.token_id'), + param('.form.type_extension.csrf.token_id'), ]) ->tag('form.type_extension') ; diff --git a/src/Symfony/Component/Form/DependencyInjection/FormPass.php b/src/Symfony/Component/Form/DependencyInjection/FormPass.php index 1d2b2a87e5c42..bec1782d40995 100644 --- a/src/Symfony/Component/Form/DependencyInjection/FormPass.php +++ b/src/Symfony/Component/Form/DependencyInjection/FormPass.php @@ -47,6 +47,7 @@ private function processFormTypes(ContainerBuilder $container): Reference // Get service locator argument $servicesMap = []; $namespaces = ['Symfony\Component\Form\Extension\Core\Type' => true]; + $csrfTokenIds = []; // Builds an array with fully-qualified type class names as keys and service IDs as values foreach ($container->findTaggedServiceIds('form.type', true) as $serviceId => $tag) { @@ -54,6 +55,10 @@ private function processFormTypes(ContainerBuilder $container): Reference $serviceDefinition = $container->getDefinition($serviceId); $servicesMap[$formType = $serviceDefinition->getClass()] = new Reference($serviceId); $namespaces[substr($formType, 0, strrpos($formType, '\\'))] = true; + + if (isset($tag[0]['csrf_token_id'])) { + $csrfTokenIds[$formType] = $tag[0]['csrf_token_id']; + } } if ($container->hasDefinition('console.command.form_debug')) { @@ -62,6 +67,14 @@ private function processFormTypes(ContainerBuilder $container): Reference $commandDefinition->setArgument(2, array_keys($servicesMap)); } + if ($csrfTokenIds && $container->hasDefinition('form.type_extension.csrf')) { + $csrfExtension = $container->getDefinition('form.type_extension.csrf'); + + if (8 <= \count($csrfExtension->getArguments())) { + $csrfExtension->replaceArgument(7, $csrfTokenIds); + } + } + return ServiceLocatorTagPass::register($container, $servicesMap); } diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php index 1cb2b0342630a..a12b9a41ee292 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php +++ b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php @@ -37,7 +37,7 @@ public function __construct( private ?string $translationDomain = null, private ?ServerParams $serverParams = null, private array $fieldAttr = [], - private ?string $defaultTokenId = null, + private string|array|null $defaultTokenId = null, ) { } @@ -50,11 +50,17 @@ public function buildForm(FormBuilderInterface $builder, array $options): void return; } + $csrfTokenId = $options['csrf_token_id'] + ?: $this->defaultTokenId[$builder->getType()->getInnerType()::class] + ?? $builder->getName() + ?: $builder->getType()->getInnerType()::class; + $builder->setAttribute('csrf_token_id', $csrfTokenId); + $builder ->addEventSubscriber(new CsrfValidationListener( $options['csrf_field_name'], $options['csrf_token_manager'], - $options['csrf_token_id'] ?: ($builder->getName() ?: $builder->getType()->getInnerType()::class), + $csrfTokenId, $options['csrf_message'], $this->translator, $this->translationDomain, @@ -70,7 +76,7 @@ public function finishView(FormView $view, FormInterface $form, array $options): { if ($options['csrf_protection'] && !$view->parent && $options['compound']) { $factory = $form->getConfig()->getFormFactory(); - $tokenId = $options['csrf_token_id'] ?: ($form->getName() ?: $form->getConfig()->getType()->getInnerType()::class); + $tokenId = $form->getConfig()->getAttribute('csrf_token_id'); $data = (string) $options['csrf_token_manager']->getToken($tokenId); $csrfForm = $factory->createNamed($options['csrf_field_name'], HiddenType::class, $data, [ @@ -85,9 +91,11 @@ public function finishView(FormView $view, FormInterface $form, array $options): public function configureOptions(OptionsResolver $resolver): void { - if ($defaultTokenId = $this->defaultTokenId) { + if (\is_string($defaultTokenId = $this->defaultTokenId) && $defaultTokenId) { $defaultTokenManager = $this->defaultTokenManager; $defaultTokenId = static fn (Options $options) => $options['csrf_token_manager'] === $defaultTokenManager ? $defaultTokenId : null; + } else { + $defaultTokenId = null; } $resolver->setDefaults([ diff --git a/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php b/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php index e9a7b50346032..f0ccd3f095fb0 100644 --- a/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php +++ b/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Command\DebugCommand; use Symfony\Component\Form\DependencyInjection\FormPass; +use Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension; use Symfony\Component\Form\FormRegistry; /** @@ -95,6 +96,25 @@ public function testAddTaggedTypesToDebugCommand() ); } + public function testAddTaggedTypesToCsrfTypeExtension() + { + $container = $this->createContainerBuilder(); + + $container->register('form.registry', FormRegistry::class); + $container->register('form.type_extension.csrf', FormTypeCsrfExtension::class) + ->setArguments([null, true, '_token', null, 'validator.translation_domain', null, [], null]) + ->setPublic(true); + + $container->setDefinition('form.extension', $this->createExtensionDefinition()); + $container->register('my.type1', __CLASS__.'_Type1')->addTag('form.type', ['csrf_token_id' => 'the_token_id']); + $container->register('my.type2', __CLASS__.'_Type2')->addTag('form.type'); + + $container->compile(); + + $csrfDefinition = $container->getDefinition('form.type_extension.csrf'); + $this->assertSame([__CLASS__.'_Type1' => 'the_token_id'], $csrfDefinition->getArgument(7)); + } + /** * @dataProvider addTaggedTypeExtensionsDataProvider */ From a60cff54b6ed544b06b349b6852bc9f6dd2fa6ae Mon Sep 17 00:00:00 2001 From: Peter van Dommelen Date: Fri, 7 Feb 2025 11:36:11 +0100 Subject: [PATCH 25/63] [DependencyInjection] Fix cloned lazy services not sharing their dependencies when dumped with PhpDumper --- .../Compiler/InlineServiceDefinitionsPass.php | 4 +- .../DependencyInjection/Dumper/PhpDumper.php | 6 ++ .../Tests/Dumper/PhpDumperTest.php | 55 +++++++++++++++++++ .../Tests/Fixtures/DependencyContainer.php | 25 +++++++++ .../Fixtures/DependencyContainerInterface.php | 17 ++++++ .../Tests/Fixtures/config/child.expected.yml | 4 +- .../config/from_callable.expected.yml | 4 +- .../Tests/Fixtures/php/closure_proxy.php | 2 +- .../Tests/Fixtures/php/lazy_closure.php | 4 +- .../php/services_almost_circular_private.php | 44 ++++++++++++--- .../php/services_almost_circular_public.php | 18 +++++- .../Fixtures/php/services_wither_lazy.php | 2 +- 12 files changed, 168 insertions(+), 17 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainer.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainerInterface.php diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index e0dc3a653e6b9..884977fff3d1f 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -224,6 +224,8 @@ private function isInlineableDefinition(string $id, Definition $definition): boo return false; } - return $this->container->getDefinition($srcId)->isShared(); + $srcDefinition = $this->container->getDefinition($srcId); + + return $srcDefinition->isShared() && !$srcDefinition->isLazy(); } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 23e65483b03d3..5f544a859ceb1 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -2185,6 +2185,12 @@ private function isSingleUsePrivateNode(ServiceReferenceGraphNode $node): bool if ($edge->isLazy() || !$value instanceof Definition || !$value->isShared()) { return false; } + + // When the source node is a proxy or ghost, it will construct its references only when the node itself is initialized. + // Since the node can be cloned before being fully initialized, we do not know how often its references are used. + if ($this->getProxyDumper()->isProxyCandidate($value)) { + return false; + } $ids[$edge->getSourceNode()->getId()] = true; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 2d0a8aad0ce16..7622d858a3110 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -55,6 +55,8 @@ use Symfony\Component\DependencyInjection\Tests\Compiler\Wither; use Symfony\Component\DependencyInjection\Tests\Compiler\WitherAnnotation; use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition; +use Symfony\Component\DependencyInjection\Tests\Fixtures\DependencyContainer; +use Symfony\Component\DependencyInjection\Tests\Fixtures\DependencyContainerInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument; @@ -1677,6 +1679,59 @@ public function testWitherWithStaticReturnType() $this->assertInstanceOf(Foo::class, $wither->foo); } + public function testCloningLazyGhostWithDependency() + { + $container = new ContainerBuilder(); + $container->register('dependency', \stdClass::class); + $container->register(DependencyContainer::class) + ->addArgument(new Reference('dependency')) + ->setLazy(true) + ->setPublic(true); + + $container->compile(); + $dumper = new PhpDumper($container); + $dump = $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Service_CloningLazyGhostWithDependency']); + eval('?>'.$dump); + + $container = new \Symfony_DI_PhpDumper_Service_CloningLazyGhostWithDependency(); + + $bar = $container->get(DependencyContainer::class); + $this->assertInstanceOf(DependencyContainer::class, $bar); + + $first_clone = clone $bar; + $second_clone = clone $bar; + + $this->assertSame($first_clone->dependency, $second_clone->dependency); + } + + public function testCloningProxyWithDependency() + { + $container = new ContainerBuilder(); + $container->register('dependency', \stdClass::class); + $container->register(DependencyContainer::class) + ->addArgument(new Reference('dependency')) + ->setLazy(true) + ->addTag('proxy', [ + 'interface' => DependencyContainerInterface::class, + ]) + ->setPublic(true); + + $container->compile(); + $dumper = new PhpDumper($container); + $dump = $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Service_CloningProxyWithDependency']); + eval('?>'.$dump); + + $container = new \Symfony_DI_PhpDumper_Service_CloningProxyWithDependency(); + + $bar = $container->get(DependencyContainer::class); + $this->assertInstanceOf(DependencyContainerInterface::class, $bar); + + $first_clone = clone $bar; + $second_clone = clone $bar; + + $this->assertSame($first_clone->getDependency(), $second_clone->getDependency()); + } + public function testCurrentFactoryInlining() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainer.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainer.php new file mode 100644 index 0000000000000..5e222bdf060be --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainer.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +class DependencyContainer implements DependencyContainerInterface +{ + public function __construct( + public mixed $dependency, + ) { + } + + public function getDependency(): mixed + { + return $this->dependency; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainerInterface.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainerInterface.php new file mode 100644 index 0000000000000..ed109cad78dcd --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainerInterface.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +interface DependencyContainerInterface +{ + public function getDependency(): mixed; +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/child.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/child.expected.yml index 44dbbd571b788..97380f388ca2a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/child.expected.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/child.expected.yml @@ -11,7 +11,9 @@ services: - container.decorator: { id: bar, inner: b } file: file.php lazy: true - arguments: [!service { class: Class1 }] + arguments: ['@b'] + b: + class: Class1 bar: alias: foo public: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/from_callable.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/from_callable.expected.yml index d4dbbbadd48bf..1ab1643af1b48 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/from_callable.expected.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/from_callable.expected.yml @@ -8,5 +8,7 @@ services: class: stdClass public: true lazy: true - arguments: [[!service { class: stdClass }, do]] + arguments: [['@bar', do]] factory: [Closure, fromCallable] + bar: + class: stdClass diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure_proxy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure_proxy.php index 2bef92604d3a9..eaf303c7d068c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure_proxy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure_proxy.php @@ -55,6 +55,6 @@ protected function createProxy($class, \Closure $factory) */ protected static function getClosureProxyService($container, $lazyLoad = true) { - return $container->services['closure_proxy'] = new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } }; + return $container->services['closure_proxy'] = new class(fn () => ($container->privates['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } }; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_closure.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_closure.php index 0af28f2650147..2bf27779df041 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_closure.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_closure.php @@ -57,7 +57,7 @@ protected function createProxy($class, \Closure $factory) */ protected static function getClosure1Service($container, $lazyLoad = true) { - return $container->services['closure1'] = (new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure { public function cloneFoo(?\stdClass $bar = null): \Symfony\Component\DependencyInjection\Tests\Compiler\Foo { return $this->service->cloneFoo(...\func_get_args()); } })->cloneFoo(...); + return $container->services['closure1'] = (new class(fn () => ($container->privates['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure { public function cloneFoo(?\stdClass $bar = null): \Symfony\Component\DependencyInjection\Tests\Compiler\Foo { return $this->service->cloneFoo(...\func_get_args()); } })->cloneFoo(...); } /** @@ -67,6 +67,6 @@ protected static function getClosure1Service($container, $lazyLoad = true) */ protected static function getClosure2Service($container, $lazyLoad = true) { - return $container->services['closure2'] = (new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\FooVoid()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure { public function __invoke(string $name): void { $this->service->__invoke(...\func_get_args()); } })->__invoke(...); + return $container->services['closure2'] = (new class(fn () => ($container->privates['foo_void'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\FooVoid())) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure { public function __invoke(string $name): void { $this->service->__invoke(...\func_get_args()); } })->__invoke(...); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php index 0a9c519c8e69c..0c234ac3934c3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php @@ -373,15 +373,13 @@ protected static function getManager2Service($container) */ protected static function getManager3Service($container, $lazyLoad = true) { - $a = ($container->services['listener3'] ?? self::getListener3Service($container)); + $a = ($container->privates['connection3'] ?? self::getConnection3Service($container)); if (isset($container->services['manager3'])) { return $container->services['manager3']; } - $b = new \stdClass(); - $b->listener = [$a]; - return $container->services['manager3'] = new \stdClass($b); + return $container->services['manager3'] = new \stdClass($a); } /** @@ -481,6 +479,34 @@ protected static function getBar6Service($container) return $container->privates['bar6'] = new \stdClass($a); } + /** + * Gets the private 'connection3' shared service. + * + * @return \stdClass + */ + protected static function getConnection3Service($container) + { + $container->privates['connection3'] = $instance = new \stdClass(); + + $instance->listener = [($container->services['listener3'] ?? self::getListener3Service($container))]; + + return $instance; + } + + /** + * Gets the private 'connection4' shared service. + * + * @return \stdClass + */ + protected static function getConnection4Service($container) + { + $container->privates['connection4'] = $instance = new \stdClass(); + + $instance->listener = [($container->services['listener4'] ?? self::getListener4Service($container))]; + + return $instance; + } + /** * Gets the private 'doctrine.listener' shared service. * @@ -572,13 +598,13 @@ protected static function getMailerInline_TransportFactory_AmazonService($contai */ protected static function getManager4Service($container, $lazyLoad = true) { - $a = new \stdClass(); + $a = ($container->privates['connection4'] ?? self::getConnection4Service($container)); - $container->privates['manager4'] = $instance = new \stdClass($a); - - $a->listener = [($container->services['listener4'] ?? self::getListener4Service($container))]; + if (isset($container->privates['manager4'])) { + return $container->privates['manager4']; + } - return $instance; + return $container->privates['manager4'] = new \stdClass($a); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php index 2250e860264dc..ae283e556a0da 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php @@ -259,7 +259,7 @@ protected static function getDispatcher2Service($container, $lazyLoad = true) { $container->services['dispatcher2'] = $instance = new \stdClass(); - $instance->subscriber2 = new \stdClass(($container->services['manager2'] ?? self::getManager2Service($container))); + $instance->subscriber2 = ($container->privates['subscriber2'] ?? self::getSubscriber2Service($container)); return $instance; } @@ -820,4 +820,20 @@ protected static function getManager4Service($container, $lazyLoad = true) return $container->privates['manager4'] = new \stdClass($a); } + + /** + * Gets the private 'subscriber2' shared service. + * + * @return \stdClass + */ + protected static function getSubscriber2Service($container) + { + $a = ($container->services['manager2'] ?? self::getManager2Service($container)); + + if (isset($container->privates['subscriber2'])) { + return $container->privates['subscriber2']; + } + + return $container->privates['subscriber2'] = new \stdClass($a); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php index f52f226597625..d5c3738a62a0b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php @@ -61,7 +61,7 @@ protected static function getWitherService($container, $lazyLoad = true) $instance = new \Symfony\Component\DependencyInjection\Tests\Compiler\Wither(); - $a = new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo(); + $a = ($container->privates['Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()); $instance = $instance->withFoo1($a); $instance = $instance->withFoo2($a); From b52b760dff21415453f242a755f7db61e837d6b0 Mon Sep 17 00:00:00 2001 From: Artem Lopata Date: Fri, 7 Feb 2025 09:04:01 +0100 Subject: [PATCH 26/63] [DependencyInjection] Do not preload functions --- .../DependencyInjection/Dumper/PhpDumper.php | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 23e65483b03d3..8719ce967e784 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -353,7 +353,7 @@ class %s extends {$options['class']} EOF; foreach ($this->preload as $class) { - if (!$class || str_contains($class, '$') || \in_array($class, ['int', 'float', 'string', 'bool', 'resource', 'object', 'array', 'null', 'callable', 'iterable', 'mixed', 'void'], true)) { + if (!$class || str_contains($class, '$') || \in_array($class, ['int', 'float', 'string', 'bool', 'resource', 'object', 'array', 'null', 'callable', 'iterable', 'mixed', 'void', 'never'], true)) { continue; } if (!(class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false)) || (new \ReflectionClass($class))->isUserDefined()) { @@ -846,8 +846,7 @@ private function addService(string $id, Definition $definition): array if ($class = $definition->getClass()) { $class = $class instanceof Parameter ? '%'.$class.'%' : $this->container->resolveEnvPlaceholders($class); $return[] = sprintf(str_starts_with($class, '%') ? '@return object A %1$s instance' : '@return \%s', ltrim($class, '\\')); - } elseif ($definition->getFactory()) { - $factory = $definition->getFactory(); + } elseif ($factory = $definition->getFactory()) { if (\is_string($factory) && !str_starts_with($factory, '@=')) { $return[] = sprintf('@return object An instance returned by %s()', $factory); } elseif (\is_array($factory) && (\is_string($factory[0]) || $factory[0] instanceof Definition || $factory[0] instanceof Reference)) { @@ -1170,9 +1169,7 @@ private function addNewInstance(Definition $definition, string $return = '', ?st $arguments[] = (\is_string($i) ? $i.': ' : '').$this->dumpValue($value); } - if (null !== $definition->getFactory()) { - $callable = $definition->getFactory(); - + if ($callable = $definition->getFactory()) { if ('current' === $callable && [0] === array_keys($definition->getArguments()) && \is_array($value) && [0] === array_keys($value)) { return $return.$this->dumpValue($value[0]).$tail; } @@ -2293,7 +2290,6 @@ private function getAutoloadFile(): ?string private function getClasses(Definition $definition, string $id): array { $classes = []; - $resolve = $this->container->getParameterBag()->resolveValue(...); while ($definition instanceof Definition) { foreach ($definition->getTag($this->preloadTags[0]) as $tag) { @@ -2305,24 +2301,24 @@ private function getClasses(Definition $definition, string $id): array } if ($class = $definition->getClass()) { - $classes[] = trim($resolve($class), '\\'); + $classes[] = trim($class, '\\'); } $factory = $definition->getFactory(); + if (\is_string($factory) && !str_starts_with($factory, '@=') && str_contains($factory, '::')) { + $factory = explode('::', $factory); + } + if (!\is_array($factory)) { - $factory = [$factory]; + $definition = $factory; + continue; } - if (\is_string($factory[0])) { - $factory[0] = $resolve($factory[0]); + $definition = $factory[0] ?? null; - if (false !== $i = strrpos($factory[0], '::')) { - $factory[0] = substr($factory[0], 0, $i); - } + if (\is_string($definition)) { $classes[] = trim($factory[0], '\\'); } - - $definition = $factory[0]; } return $classes; From 416aa0e86f971cbf3a4e28b3c3fab76703608499 Mon Sep 17 00:00:00 2001 From: Hugo Posnic Date: Fri, 29 Nov 2024 11:17:22 +0100 Subject: [PATCH 27/63] [WebProfilerBundle] Fix interception for non conventional redirects --- .../WebProfilerBundle/EventListener/WebDebugToolbarListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php index c2b350ff05d68..87cb3d55fe42f 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -107,7 +107,7 @@ public function onKernelResponse(ResponseEvent $event): void return; } - if ($response->headers->has('X-Debug-Token') && $response->isRedirect() && $this->interceptRedirects && 'html' === $request->getRequestFormat()) { + if ($response->headers->has('X-Debug-Token') && $response->isRedirect() && $this->interceptRedirects && 'html' === $request->getRequestFormat() && $response->headers->has('Location')) { if ($request->hasSession() && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag) { // keep current flashes for one more request if using AutoExpireFlashBag $session->getFlashBag()->setAll($session->getFlashBag()->peekAll()); From 4121f68cac530203d1d46fa893ac50eaebb9f432 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 10 Feb 2025 15:27:56 +0100 Subject: [PATCH 28/63] [Notifier] [BlueSky] Change the value returned as the message ID --- .../Bridge/Bluesky/BlueskyTransport.php | 2 +- .../Notifier/Bridge/Bluesky/CHANGELOG.md | 1 + .../Bluesky/Tests/BlueskyTransportTest.php | 31 ++++++++++++++++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php b/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php index 3233159a28a0a..db8eff9e70f50 100644 --- a/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php @@ -105,7 +105,7 @@ protected function doSend(MessageInterface $message): SentMessage if (200 === $statusCode) { $content = $response->toArray(); $sentMessage = new SentMessage($message, (string) $this); - $sentMessage->setMessageId($content['cid']); + $sentMessage->setMessageId($content['uri']); return $sentMessage; } diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md index d337db00df015..b4b57416c470c 100644 --- a/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add option to attach a media + * [BC Break] Change the returned message ID from record's 'cid' to 'uri' 7.1 --- diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php index 1cfa099e04537..09323174d8ce3 100644 --- a/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php @@ -276,7 +276,11 @@ public function testParseFacetsUrlWithTrickyRegex() public function testWithMedia() { - $transport = $this->createTransport(new MockHttpClient((function () { + // realistic sample values taken from https://docs.bsky.app/docs/advanced-guides/posts#post-record-structure + $recordUri = 'at://did:plc:u5cwb2mwiv2bfq53cjufe6yn/app.bsky.feed.post/3k4duaz5vfs2b'; + $recordCid = 'bafyreibjifzpqj6o6wcq3hejh7y4z4z2vmiklkvykc57tw3pcbx3kxifpm'; + + $transport = $this->createTransport(new MockHttpClient((function () use ($recordUri, $recordCid) { yield function (string $method, string $url, array $options) { $this->assertSame('POST', $method); $this->assertSame('https://bsky.social/xrpc/com.atproto.server.createSession', $url); @@ -299,13 +303,13 @@ public function testWithMedia() ]]); }; - yield function (string $method, string $url, array $options) { + yield function (string $method, string $url, array $options) use ($recordUri, $recordCid) { $this->assertSame('POST', $method); $this->assertSame('https://bsky.social/xrpc/com.atproto.repo.createRecord', $url); $this->assertArrayHasKey('authorization', $options['normalized_headers']); $this->assertSame('{"repo":null,"collection":"app.bsky.feed.post","record":{"$type":"app.bsky.feed.post","text":"Hello World!","createdAt":"2024-04-28T08:40:17.000000Z","embed":{"$type":"app.bsky.embed.images","images":[{"alt":"A fixture","image":{"$type":"blob","ref":{"$link":"bafkreibabalobzn6cd366ukcsjycp4yymjymgfxcv6xczmlgpemzkz3cfa"},"mimeType":"image\/png","size":760898}}]}}}', $options['body']); - return new JsonMockResponse(['cid' => '103254962155278888']); + return new JsonMockResponse(['uri' => $recordUri, 'cid' => $recordCid]); }; })())); @@ -313,7 +317,26 @@ public function testWithMedia() ->attachMedia(new File(__DIR__.'/fixtures.gif'), 'A fixture'); $result = $transport->send(new ChatMessage('Hello World!', $options)); - $this->assertSame('103254962155278888', $result->getMessageId()); + $this->assertSame($recordUri, $result->getMessageId()); + } + + public function testReturnedMessageId() + { + // realistic sample values taken from https://docs.bsky.app/docs/advanced-guides/posts#post-record-structure + $recordUri = 'at://did:plc:u5cwb2mwiv2bfq53cjufe6yn/app.bsky.feed.post/3k4duaz5vfs2b'; + $recordCid = 'bafyreibjifzpqj6o6wcq3hejh7y4z4z2vmiklkvykc57tw3pcbx3kxifpm'; + + $client = new MockHttpClient(function () use ($recordUri, $recordCid) { + return new JsonMockResponse([ + 'uri' => $recordUri, + 'cid' => $recordCid, + ]); + }); + + $transport = self::createTransport($client); + $message = $transport->send(new ChatMessage('Hello!')); + + $this->assertSame($recordUri, $message->getMessageId()); } /** From 0fbfc3e32de07cc56d1f95d5704af4bf5487d372 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 11 Feb 2025 14:07:09 +0100 Subject: [PATCH 29/63] [BrowserKit] Fix submitting forms with empty file fields --- .../Component/BrowserKit/HttpBrowser.php | 9 +++-- .../BrowserKit/Tests/HttpBrowserTest.php | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/BrowserKit/HttpBrowser.php b/src/Symfony/Component/BrowserKit/HttpBrowser.php index 9d84bda751ba5..4eb30b5be9ba0 100644 --- a/src/Symfony/Component/BrowserKit/HttpBrowser.php +++ b/src/Symfony/Component/BrowserKit/HttpBrowser.php @@ -143,10 +143,15 @@ private function getUploadedFiles(array $files): array } if (!isset($file['tmp_name'])) { $uploadedFiles[$name] = $this->getUploadedFiles($file); + continue; } - if (isset($file['tmp_name'])) { - $uploadedFiles[$name] = DataPart::fromPath($file['tmp_name'], $file['name']); + + if ('' === $file['tmp_name']) { + $uploadedFiles[$name] = new DataPart('', ''); + continue; } + + $uploadedFiles[$name] = DataPart::fromPath($file['tmp_name'], $file['name']); } return $uploadedFiles; diff --git a/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php b/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php index e1f19b16ce814..3a2547d89f488 100644 --- a/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php +++ b/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php @@ -14,6 +14,8 @@ use Symfony\Component\BrowserKit\CookieJar; use Symfony\Component\BrowserKit\History; use Symfony\Component\BrowserKit\HttpBrowser; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -208,6 +210,37 @@ public static function forwardSlashesRequestPathProvider() ]; } + public function testEmptyUpload() + { + $client = new MockHttpClient(function ($method, $url, $options) { + $this->assertSame('POST', $method); + $this->assertSame('http://localhost/', $url); + $this->assertStringStartsWith('Content-Type: multipart/form-data; boundary=', $options['normalized_headers']['content-type'][0]); + + $body = ''; + while ('' !== $data = $options['body'](1024)) { + $body .= $data; + } + + $expected = <<assertStringMatchesFormat($expected, $body); + + return new MockResponse(); + }); + + $browser = new HttpBrowser($client); + $browser->request('POST', '/', [], ['file' => ['tmp_name' => '', 'name' => 'file']]); + } + private function uploadFile(string $data): string { $path = tempnam(sys_get_temp_dir(), 'http'); From 159e6b9154696bfbac320e768156b4e633a81ed1 Mon Sep 17 00:00:00 2001 From: DemigodCode Date: Tue, 11 Feb 2025 13:06:06 +0100 Subject: [PATCH 30/63] [Cache] Tests for Redis Replication with cache --- .github/workflows/integration-tests.yml | 12 ++++ .../PredisRedisReplicationAdapterTest.php | 29 +++++++++ .../Adapter/PredisReplicationAdapterTest.php | 24 +++++++ .../PredisTagAwareReplicationAdapterTest.php | 37 +++++++++++ .../Adapter/RedisReplicationAdapterTest.php | 65 +++++++++++++++++++ .../Component/Cache/Traits/RedisTrait.php | 14 +++- 6 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php create mode 100644 src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php create mode 100644 src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php create mode 100644 src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 29b9b8ec62409..5c2839d74f818 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -81,6 +81,17 @@ jobs: REDIS_MASTER_HOST: redis REDIS_MASTER_SET: redis_sentinel REDIS_SENTINEL_QUORUM: 1 + redis-primary: + image: redis:latest + hostname: redis-primary + ports: + - 16381:6379 + + redis-replica: + image: redis:latest + ports: + - 16382:6379 + command: redis-server --slaveof redis-primary 6379 memcached: image: memcached:1.6.5 ports: @@ -239,6 +250,7 @@ jobs: REDIS_CLUSTER_HOSTS: 'localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005' REDIS_SENTINEL_HOSTS: 'unreachable-host:26379 localhost:26379 localhost:26379' REDIS_SENTINEL_SERVICE: redis_sentinel + REDIS_REPLICATION_HOSTS: 'localhost:16381 localhost:16382' MESSENGER_REDIS_DSN: redis://127.0.0.1:7006/messages MESSENGER_AMQP_DSN: amqp://localhost/%2f/messages MESSENGER_SQS_DSN: "sqs://localhost:4566/messages?sslmode=disable&poll_timeout=0.01" diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php new file mode 100644 index 0000000000000..552727740c18b --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\RedisAdapter; + +/** + * @group integration + */ +class PredisRedisReplicationAdapterTest extends AbstractRedisAdapterTestCase +{ + public static function setUpBeforeClass(): void + { + if (!$hosts = getenv('REDIS_REPLICATION_HOSTS')) { + self::markTestSkipped('REDIS_REPLICATION_HOSTS env var is not defined.'); + } + + self::$redis = RedisAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).'][alias]=master', ['class' => \Predis\Client::class, 'prefix' => 'prefix_']); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php new file mode 100644 index 0000000000000..4add9d5f18c4a --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +/** + * @group integration + */ +class PredisReplicationAdapterTest extends AbstractRedisAdapterTestCase +{ + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + self::$redis = new \Predis\Client(array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => 6379]), ['prefix' => 'prefix_']); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php new file mode 100644 index 0000000000000..4d8651ce4ceb6 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; + +/** + * @group integration + */ +class PredisTagAwareReplicationAdapterTest extends PredisReplicationAdapterTest +{ + use TagAwareTestTrait; + + protected function setUp(): void + { + parent::setUp(); + $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; + } + + public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface + { + $this->assertInstanceOf(\Predis\Client::class, self::$redis); + $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); + + return $adapter; + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php new file mode 100644 index 0000000000000..e41745057f141 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Traits\RedisClusterProxy; + +/** + * @group integration + */ +class RedisReplicationAdapterTest extends AbstractRedisAdapterTestCase +{ + public static function setUpBeforeClass(): void + { + if (!$hosts = getenv('REDIS_REPLICATION_HOSTS')) { + self::markTestSkipped('REDIS_REPLICATION_HOSTS env var is not defined.'); + } + + self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).'][alias]=master', ['lazy' => true]); + self::$redis->setOption(\Redis::OPT_PREFIX, 'prefix_'); + } + + public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface + { + if ('testClearWithPrefix' === $testMethod && \defined('Redis::SCAN_PREFIX')) { + self::$redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_PREFIX); + } + + $this->assertInstanceOf(RedisClusterProxy::class, self::$redis); + $adapter = new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); + + return $adapter; + } + + /** + * @dataProvider provideFailedCreateConnection + */ + public function testFailedCreateConnection(string $dsn) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Redis connection '); + RedisAdapter::createConnection($dsn); + } + + public static function provideFailedCreateConnection(): array + { + return [ + ['redis://localhost:1234'], + ['redis://foo@localhost?role=master'], + ['redis://localhost/123?role=master'], + ]; + } +} diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index fc8f5cec60472..2ebaed16f1804 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -17,6 +17,7 @@ use Predis\Connection\Aggregate\ReplicationInterface; use Predis\Connection\Cluster\ClusterInterface as Predis2ClusterInterface; use Predis\Connection\Cluster\RedisCluster as Predis2RedisCluster; +use Predis\Connection\Replication\ReplicationInterface as Predis2ReplicationInterface; use Predis\Response\ErrorInterface; use Predis\Response\Status; use Relay\Relay; @@ -473,9 +474,16 @@ protected function doClear(string $namespace): bool $cleared = true; $hosts = $this->getHosts(); $host = reset($hosts); - if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) { - // Predis supports info command only on the master in replication environments - $hosts = [$host->getClientFor('master')]; + if ($host instanceof \Predis\Client) { + $connection = $host->getConnection(); + + if ($connection instanceof ReplicationInterface) { + $hosts = [$host->getClientFor('master')]; + } elseif ($connection instanceof Predis2ReplicationInterface) { + $connection->switchToMaster(); + + $hosts = [$host]; + } } foreach ($hosts as $host) { From 5cf6c66409bebdac24b9adaa4a35f608407ba960 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 11 Feb 2025 17:41:13 +0100 Subject: [PATCH 31/63] [WebProfilerBundle] Fix tests --- .../Tests/EventListener/WebDebugToolbarListenerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php index 33bf1a32d27f8..cf3c189204301 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php @@ -63,6 +63,7 @@ public static function getInjectToolbarTests() public function testHtmlRedirectionIsIntercepted($statusCode) { $response = new Response('Some content', $statusCode); + $response->headers->set('Location', 'https://example.com/'); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -76,6 +77,7 @@ public function testHtmlRedirectionIsIntercepted($statusCode) public function testNonHtmlRedirectionIsNotIntercepted() { $response = new Response('Some content', '301'); + $response->headers->set('Location', 'https://example.com/'); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request([], [], ['_format' => 'json']), HttpKernelInterface::MAIN_REQUEST, $response); @@ -139,6 +141,7 @@ public function testToolbarIsNotInjectedOnContentDispositionAttachment() public function testToolbarIsNotInjectedOnRedirection($statusCode) { $response = new Response('', $statusCode); + $response->headers->set('Location', 'https://example.com/'); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); From 74baaf08ce464ce649fde7fd8478250e0c5f81c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Pr=C3=A9vot?= Date: Wed, 12 Feb 2025 03:01:53 +0100 Subject: [PATCH 32/63] ntfy-notifier: tfix in description Bug-Debian: https://bugs.debian.org/1095786 --- src/Symfony/Component/Notifier/Bridge/Ntfy/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Ntfy/composer.json b/src/Symfony/Component/Notifier/Bridge/Ntfy/composer.json index 5fdbc79b1cd21..988a4ce85efb4 100644 --- a/src/Symfony/Component/Notifier/Bridge/Ntfy/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Ntfy/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/ntfy-notifier", "type": "symfony-notifier-bridge", - "description": "Symfony Ntyf Notifier Bridge", + "description": "Symfony Ntfy Notifier Bridge", "keywords": ["ntfy", "notifier"], "homepage": "https://symfony.com", "license": "MIT", From 42f9aa4e5262bc2d807ca7f638c051b340e269da Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 12 Feb 2025 11:29:03 +0100 Subject: [PATCH 33/63] [HttpClient] fix merge up --- src/Symfony/Component/HttpClient/Response/AmpResponseV5.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpClient/Response/AmpResponseV5.php b/src/Symfony/Component/HttpClient/Response/AmpResponseV5.php index 4f70851945ac4..8f56c76a41033 100644 --- a/src/Symfony/Component/HttpClient/Response/AmpResponseV5.php +++ b/src/Symfony/Component/HttpClient/Response/AmpResponseV5.php @@ -162,7 +162,7 @@ private static function schedule(self $response, array &$runningResponses): void /** * @param AmpClientStateV5 $multi */ - private static function perform(ClientState $multi, ?array &$responses = null): void + private static function perform(ClientState $multi, ?array $responses = null): void { if ($responses) { foreach ($responses as $response) { From c795ab4bc9c9054ee293c878ad63ea23d8764fd9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 12 Feb 2025 15:02:41 +0100 Subject: [PATCH 34/63] Fix merge --- .../Tests/Fixtures/php/callable_adapter_consumer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php index ccd8d2e0bf63b..216dca434e489 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php @@ -50,6 +50,6 @@ public function getRemovedIds(): array */ protected static function getBarService($container) { - return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\CallableAdapterConsumer(new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } }); + return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\CallableAdapterConsumer(new class(fn () => (new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } }); } } From 707c3d7632004fa3b6f2a3b5f1e3ebb9993d5e3f Mon Sep 17 00:00:00 2001 From: David Vancl <43696921+davidvancl@users.noreply.github.com> Date: Tue, 11 Feb 2025 09:36:58 +0100 Subject: [PATCH 35/63] [Translation] check empty notes --- .../Translation/Dumper/XliffFileDumper.php | 2 +- .../Tests/Dumper/XliffFileDumperTest.php | 36 +++++++++++++++++++ .../Fixtures/resources-2.0-empty-notes.xlf | 20 +++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-empty-notes.xlf diff --git a/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php b/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php index d0f016b23c365..f5ce96a02580c 100644 --- a/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php +++ b/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php @@ -176,7 +176,7 @@ private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ? $metadata = $messages->getMetadata($source, $domain); // Add notes section - if ($this->hasMetadataArrayInfo('notes', $metadata)) { + if ($this->hasMetadataArrayInfo('notes', $metadata) && $metadata['notes']) { $notesElement = $dom->createElement('notes'); foreach ($metadata['notes'] as $note) { $n = $dom->createElement('note'); diff --git a/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php b/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php index f9ae8986f52fe..a1a125f605880 100644 --- a/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php +++ b/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php @@ -147,4 +147,40 @@ public function testDumpCatalogueWithXliffExtension() $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR']) ); } + + public function testFormatCatalogueXliff2WithSegmentAttributes() + { + $catalogue = new MessageCatalogue('en_US'); + $catalogue->add([ + 'foo' => 'bar', + 'key' => '', + ]); + $catalogue->setMetadata('foo', ['segment-attributes' => ['state' => 'translated']]); + $catalogue->setMetadata('key', ['segment-attributes' => ['state' => 'translated', 'subState' => 'My Value']]); + + $dumper = new XliffFileDumper(); + + $this->assertStringEqualsFile( + __DIR__.'/../Fixtures/resources-2.0-segment-attributes.xlf', + $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR', 'xliff_version' => '2.0']) + ); + } + + public function testEmptyMetadataNotes() + { + $catalogue = new MessageCatalogue('en_US'); + $catalogue->add([ + 'empty' => 'notes', + 'full' => 'notes', + ]); + $catalogue->setMetadata('empty', ['notes' => []]); + $catalogue->setMetadata('full', ['notes' => [['category' => 'file-source', 'priority' => 1, 'content' => 'test/path/to/translation/Example.1.html.twig:27']]]); + + $dumper = new XliffFileDumper(); + + $this->assertStringEqualsFile( + __DIR__.'/../Fixtures/resources-2.0-empty-notes.xlf', + $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR', 'xliff_version' => '2.0']) + ); + } } diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-empty-notes.xlf b/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-empty-notes.xlf new file mode 100644 index 0000000000000..7cba2cc09b41e --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-empty-notes.xlf @@ -0,0 +1,20 @@ + + + + + + empty + notes + + + + + test/path/to/translation/Example.1.html.twig:27 + + + full + notes + + + + From 540253a89b7a90130f683091558a8b167e425e13 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 12 Feb 2025 17:44:25 +0100 Subject: [PATCH 36/63] [VarExporter] Fix lazy objects with hooked properties --- .../VarExporter/Internal/Hydrator.php | 2 + .../Internal/LazyObjectRegistry.php | 17 ++- .../Component/VarExporter/ProxyHelper.php | 109 +++++++++++++++++- .../VarExporter/Tests/Fixtures/Hooked.php | 25 ++++ .../VarExporter/Tests/LazyGhostTraitTest.php | 26 +++++ .../VarExporter/Tests/LazyProxyTraitTest.php | 28 +++++ .../VarExporter/Tests/ProxyHelperTest.php | 12 ++ 7 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php diff --git a/src/Symfony/Component/VarExporter/Internal/Hydrator.php b/src/Symfony/Component/VarExporter/Internal/Hydrator.php index 49d636fb8e0ce..97ffe4c831627 100644 --- a/src/Symfony/Component/VarExporter/Internal/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Internal/Hydrator.php @@ -287,6 +287,8 @@ public static function getPropertyScopes($class) if (\ReflectionProperty::IS_PROTECTED & $flags) { $propertyScopes["\0*\0$name"] = $propertyScopes[$name]; + } elseif (\PHP_VERSION_ID >= 80400 && $property->getHooks()) { + $propertyScopes[$name][] = true; } } diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php index fddc6fb3b9664..a7b4987e3b0db 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php @@ -50,6 +50,7 @@ class LazyObjectRegistry public static function getClassResetters($class) { $classProperties = []; + $hookedProperties = []; if ((self::$classReflectors[$class] ??= new \ReflectionClass($class))->isInternal()) { $propertyScopes = []; @@ -60,7 +61,13 @@ public static function getClassResetters($class) foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) { $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; - if ($k === $key && "\0$class\0lazyObjectState" !== $k) { + if ($k !== $key || "\0$class\0lazyObjectState" === $k) { + continue; + } + + if ($k === $name && ($propertyScopes[$k][4] ?? false)) { + $hookedProperties[$k] = true; + } else { $classProperties[$readonlyScope ?? $scope][$name] = $key; } } @@ -76,9 +83,13 @@ public static function getClassResetters($class) }, null, $scope); } - $resetters[] = static function ($instance, $skippedProperties, $onlyProperties = null) { + $resetters[] = static function ($instance, $skippedProperties, $onlyProperties = null) use ($hookedProperties) { foreach ((array) $instance as $name => $value) { - if ("\0" !== ($name[0] ?? '') && !\array_key_exists($name, $skippedProperties) && (null === $onlyProperties || \array_key_exists($name, $onlyProperties))) { + if ("\0" !== ($name[0] ?? '') + && !\array_key_exists($name, $skippedProperties) + && (null === $onlyProperties || \array_key_exists($name, $onlyProperties)) + && !isset($hookedProperties[$name]) + ) { unset($instance->$name); } } diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index d5a8d7418b807..246dc4d404bc7 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -58,6 +58,37 @@ public static function generateLazyGhost(\ReflectionClass $class): string throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" extends "%s" which is internal.', $class->name, $parent->name)); } } + + $hooks = ''; + $propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name); + foreach ($propertyScopes as $name => $scope) { + if (!isset($scope[4]) || ($p = $scope[3])->isVirtual()) { + continue; + } + + $type = self::exportType($p); + $hooks .= "\n public {$type} \${$name} {\n"; + + foreach ($p->getHooks() as $hook => $method) { + if ($method->isFinal()) { + throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is final.', $class->name, $method->name)); + } + + if ('get' === $hook) { + $ref = ($method->returnsReference() ? '&' : ''); + $hooks .= " {$ref}get { \$this->initializeLazyObject(); return parent::\${$name}::get(); }\n"; + } elseif ('set' === $hook) { + $parameters = self::exportParameters($method, true); + $arg = '$'.$method->getParameters()[0]->name; + $hooks .= " set({$parameters}) { \$this->initializeLazyObject(); parent::\${$name}::set({$arg}); }\n"; + } else { + throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is not supported.', $class->name, $method->name)); + } + } + + $hooks .= " }\n"; + } + $propertyScopes = self::exportPropertyScopes($class->name); return <<name)); } + $hookedProperties = []; + if (\PHP_VERSION_ID >= 80400 && $class) { + $propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name); + foreach ($propertyScopes as $name => $scope) { + if (isset($scope[4]) && !($p = $scope[3])->isVirtual()) { + $hookedProperties[$name] = [$p, $p->getHooks()]; + } + } + } + $methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) ?? []]; foreach ($interfaces as $interface) { if (!$interface->isInterface()) { throw new LogicException(sprintf('Cannot generate lazy proxy: "%s" is not an interface.', $interface->name)); } $methodReflectors[] = $interface->getMethods(); + + if (\PHP_VERSION_ID >= 80400 && !$class) { + foreach ($interface->getProperties() as $p) { + $hookedProperties[$p->name] ??= [$p, []]; + $hookedProperties[$p->name][1] += $p->getHooks(); + } + } + } + + $hooks = ''; + foreach ($hookedProperties as $name => [$p, $methods]) { + $type = self::exportType($p); + $hooks .= "\n public {$type} \${$p->name} {\n"; + + foreach ($methods as $hook => $method) { + if ($method->isFinal()) { + throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is final.', $class->name, $method->name)); + } + + if ('get' === $hook) { + $ref = ($method->returnsReference() ? '&' : ''); + $hooks .= <<lazyObjectState)) { + return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$p->name}; + } + + return parent::\${$p->name}::get(); + } + + EOPHP; + } elseif ('set' === $hook) { + $parameters = self::exportParameters($method, true); + $arg = '$'.$method->getParameters()[0]->name; + $hooks .= <<lazyObjectState)) { + \$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)(); + \$this->lazyObjectState->realInstance->{$p->name} = {$arg}; + } + + parent::\${$p->name}::set({$arg}); + } + + EOPHP; + } else { + throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is not supported.', $class->name, $method->name)); + } + } + + $hooks .= " }\n"; } - $methodReflectors = array_merge(...$methodReflectors); $extendsInternalClass = false; if ($parent = $class) { @@ -112,6 +203,7 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf } $methodsHaveToBeProxied = $extendsInternalClass; $methods = []; + $methodReflectors = array_merge(...$methodReflectors); foreach ($methodReflectors as $method) { if ('__get' !== strtolower($method->name) || 'mixed' === ($type = self::exportType($method) ?? 'mixed')) { @@ -228,7 +320,7 @@ public function __unserialize(\$data): void {$lazyProxyTraitStatement} private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes}; - {$body}} + {$hooks}{$body}} // Help opcache.preload discover always-needed symbols class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); @@ -238,7 +330,7 @@ class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); EOPHP; } - public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string + public static function exportParameters(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string { $byRefIndex = 0; $args = ''; @@ -268,8 +360,15 @@ public static function exportSignature(\ReflectionFunctionAbstract $function, bo $args = implode(', ', $args); } + return implode(', ', $parameters); + } + + public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string + { + $parameters = self::exportParameters($function, $withParameterTypes, $args); + $signature = 'function '.($function->returnsReference() ? '&' : '') - .($function->isClosure() ? '' : $function->name).'('.implode(', ', $parameters).')'; + .($function->isClosure() ? '' : $function->name).'('.$parameters.')'; if ($function instanceof \ReflectionMethod) { $signature = ($function->isPublic() ? 'public ' : ($function->isProtected() ? 'protected ' : 'private ')) diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php new file mode 100644 index 0000000000000..0c46d37afe922 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures; + +class Hooked +{ + public int $notBacked { + get { return 123; } + set { throw \LogicException('Cannot set value.'); } + } + + public int $backed { + get { return $this->backed ??= 234; } + set { $this->backed = $value; } + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index 68e76a7dac1fa..00f090a43c292 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\VarExporter\Internal\LazyObjectState; use Symfony\Component\VarExporter\ProxyHelper; +use Symfony\Component\VarExporter\Tests\Fixtures\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildMagicClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildStdClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildTestClass; @@ -478,6 +479,31 @@ public function testNormalization() $this->assertSame(['property' => 'property', 'method' => 'method'], $output); } + /** + * @requires PHP 8.4 + */ + public function testPropertyHooks() + { + $initialized = false; + $object = $this->createLazyGhost(Hooked::class, function ($instance) use (&$initialized) { + $initialized = true; + }); + + $this->assertSame(123, $object->notBacked); + $this->assertFalse($initialized); + $this->assertSame(234, $object->backed); + $this->assertTrue($initialized); + + $initialized = false; + $object = $this->createLazyGhost(Hooked::class, function ($instance) use (&$initialized) { + $initialized = true; + }); + + $object->backed = 345; + $this->assertTrue($initialized); + $this->assertSame(345, $object->backed); + } + /** * @template T * diff --git a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php index c4234d085b6dc..938b304461291 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php @@ -18,6 +18,7 @@ use Symfony\Component\VarExporter\Exception\LogicException; use Symfony\Component\VarExporter\LazyProxyTrait; use Symfony\Component\VarExporter\ProxyHelper; +use Symfony\Component\VarExporter\Tests\Fixtures\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\FinalPublicClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\ReadOnlyClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass; @@ -298,6 +299,33 @@ public function testNormalization() $this->assertSame(['property' => 'property', 'method' => 'method'], $output); } + /** + * @requires PHP 8.4 + */ + public function testPropertyHooks() + { + $initialized = false; + $object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) { + $initialized = true; + return new Hooked(); + }); + + $this->assertSame(123, $object->notBacked); + $this->assertFalse($initialized); + $this->assertSame(234, $object->backed); + $this->assertTrue($initialized); + + $initialized = false; + $object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) { + $initialized = true; + return new Hooked(); + }); + + $object->backed = 345; + $this->assertTrue($initialized); + $this->assertSame(345, $object->backed); + } + /** * @template T * diff --git a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php index b7372632de217..d0085a70498c5 100644 --- a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php +++ b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\VarExporter\Exception\LogicException; use Symfony\Component\VarExporter\ProxyHelper; +use Symfony\Component\VarExporter\Tests\Fixtures\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Php82NullStandaloneReturnType; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass; @@ -246,6 +247,17 @@ public function testNullStandaloneReturnType() ProxyHelper::generateLazyProxy(new \ReflectionClass(Php82NullStandaloneReturnType::class)) ); } + + /** + * @requires PHP 8.4 + */ + public function testPropertyHooks() + { + self::assertStringContainsString( + "[parent::class, 'backed', null, 4 => true]", + ProxyHelper::generateLazyProxy(new \ReflectionClass(Hooked::class)) + ); + } } abstract class TestForProxyHelper From 4a475e09ccb454357dfd1b677cbc44bcb2b8260e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 13 Feb 2025 10:55:13 +0100 Subject: [PATCH 37/63] [HttpClient] Don't send any default content-type when the body is empty --- src/Symfony/Component/HttpClient/HttpClientTrait.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index 89446ff8cfd78..4be9afa798296 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -356,9 +356,11 @@ private static function normalizeBody($body, array &$normalizedHeaders = []) } }); - $body = http_build_query($body, '', '&'); + if ('' === $body = http_build_query($body, '', '&')) { + return ''; + } - if ('' === $body || !$streams && !str_contains($normalizedHeaders['content-type'][0] ?? '', 'multipart/form-data')) { + if (!$streams && !str_contains($normalizedHeaders['content-type'][0] ?? '', 'multipart/form-data')) { if (!str_contains($normalizedHeaders['content-type'][0] ?? '', 'application/x-www-form-urlencoded')) { $normalizedHeaders['content-type'] = ['Content-Type: application/x-www-form-urlencoded']; } From 7b9968e16e9d21871857954811f8eab409808f5f Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Thu, 13 Feb 2025 10:59:49 +0100 Subject: [PATCH 38/63] Fixes XliffFileDumperTest for 6.4 --- .../Tests/Dumper/XliffFileDumperTest.php | 18 ------------------ .../Fixtures/resources-2.0-empty-notes.xlf | 4 ++-- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php b/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php index a1a125f605880..ed016304110d0 100644 --- a/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php +++ b/src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php @@ -148,24 +148,6 @@ public function testDumpCatalogueWithXliffExtension() ); } - public function testFormatCatalogueXliff2WithSegmentAttributes() - { - $catalogue = new MessageCatalogue('en_US'); - $catalogue->add([ - 'foo' => 'bar', - 'key' => '', - ]); - $catalogue->setMetadata('foo', ['segment-attributes' => ['state' => 'translated']]); - $catalogue->setMetadata('key', ['segment-attributes' => ['state' => 'translated', 'subState' => 'My Value']]); - - $dumper = new XliffFileDumper(); - - $this->assertStringEqualsFile( - __DIR__.'/../Fixtures/resources-2.0-segment-attributes.xlf', - $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR', 'xliff_version' => '2.0']) - ); - } - public function testEmptyMetadataNotes() { $catalogue = new MessageCatalogue('en_US'); diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-empty-notes.xlf b/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-empty-notes.xlf index 7cba2cc09b41e..edda607139eee 100644 --- a/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-empty-notes.xlf +++ b/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-empty-notes.xlf @@ -1,13 +1,13 @@ - + empty notes - + test/path/to/translation/Example.1.html.twig:27 From 5953ff72731c253646da1d31750c4fea59f9441b Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 14 Feb 2025 10:47:54 +0100 Subject: [PATCH 39/63] [TwigBridge] Fix compatibility with Twig 3.21 --- .../Twig/TokenParser/DumpTokenParser.php | 18 +++++++++++++++++- .../Twig/TokenParser/FormThemeTokenParser.php | 10 +++++++--- .../Twig/TokenParser/StopwatchTokenParser.php | 4 +++- .../TransDefaultDomainTokenParser.php | 4 +++- .../Twig/TokenParser/TransTokenParser.php | 12 ++++++++---- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php index e671f9ba0b7dd..9c12dc23dfba5 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php @@ -14,6 +14,7 @@ use Symfony\Bridge\Twig\Node\DumpNode; use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Token; use Twig\TokenParser\AbstractTokenParser; @@ -34,13 +35,28 @@ public function parse(Token $token): Node { $values = null; if (!$this->parser->getStream()->test(Token::BLOCK_END_TYPE)) { - $values = $this->parser->getExpressionParser()->parseMultitargetExpression(); + $values = method_exists($this->parser, 'parseExpression') ? + $this->parseMultitargetExpression() : + $this->parser->getExpressionParser()->parseMultitargetExpression(); } $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); return new DumpNode(class_exists(LocalVariable::class) ? new LocalVariable(null, $token->getLine()) : $this->parser->getVarName(), $values, $token->getLine(), $this->getTag()); } + private function parseMultitargetExpression(): Node + { + $targets = []; + while (true) { + $targets[] = $this->parser->parseExpression(); + if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Nodes($targets); + } + public function getTag(): string { return 'dump'; diff --git a/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php index b95a2a05e76a4..c5fc2311e5eec 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php @@ -29,12 +29,16 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $form = $this->parser->getExpressionParser()->parseExpression(); + $parseExpression = method_exists($this->parser, 'parseExpression') + ? $this->parser->parseExpression(...) + : $this->parser->getExpressionParser()->parseExpression(...); + + $form = $parseExpression(); $only = false; if ($this->parser->getStream()->test(Token::NAME_TYPE, 'with')) { $this->parser->getStream()->next(); - $resources = $this->parser->getExpressionParser()->parseExpression(); + $resources = $parseExpression(); if ($this->parser->getStream()->nextIf(Token::NAME_TYPE, 'only')) { $only = true; @@ -42,7 +46,7 @@ public function parse(Token $token): Node } else { $resources = new ArrayExpression([], $stream->getCurrent()->getLine()); do { - $resources->addElement($this->parser->getExpressionParser()->parseExpression()); + $resources->addElement($parseExpression()); } while (!$stream->test(Token::BLOCK_END_TYPE)); } diff --git a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php index 324f9d453ac78..c478d9e6d783f 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php @@ -38,7 +38,9 @@ public function parse(Token $token): Node $stream = $this->parser->getStream(); // {% stopwatch 'bar' %} - $name = $this->parser->getExpressionParser()->parseExpression(); + $name = method_exists($this->parser, 'parseExpression') ? + $this->parser->parseExpression() : + $this->parser->getExpressionParser()->parseExpression(); $stream->expect(Token::BLOCK_END_TYPE); diff --git a/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php index c6d850d07cbf7..a64a2332810e7 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php @@ -25,7 +25,9 @@ final class TransDefaultDomainTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = method_exists($this->parser, 'parseExpression') ? + $this->parser->parseExpression() : + $this->parser->getExpressionParser()->parseExpression(); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); diff --git a/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php index e60263a4a783f..2d17c9da70ab3 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php @@ -36,29 +36,33 @@ public function parse(Token $token): Node $vars = new ArrayExpression([], $lineno); $domain = null; $locale = null; + $parseExpression = method_exists($this->parser, 'parseExpression') + ? $this->parser->parseExpression(...) + : $this->parser->getExpressionParser()->parseExpression(...); + if (!$stream->test(Token::BLOCK_END_TYPE)) { if ($stream->test('count')) { // {% trans count 5 %} $stream->next(); - $count = $this->parser->getExpressionParser()->parseExpression(); + $count = $parseExpression(); } if ($stream->test('with')) { // {% trans with vars %} $stream->next(); - $vars = $this->parser->getExpressionParser()->parseExpression(); + $vars = $parseExpression(); } if ($stream->test('from')) { // {% trans from "messages" %} $stream->next(); - $domain = $this->parser->getExpressionParser()->parseExpression(); + $domain = $parseExpression(); } if ($stream->test('into')) { // {% trans into "fr" %} $stream->next(); - $locale = $this->parser->getExpressionParser()->parseExpression(); + $locale = $parseExpression(); } elseif (!$stream->test(Token::BLOCK_END_TYPE)) { throw new SyntaxError('Unexpected token. Twig was looking for the "with", "from", or "into" keyword.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); } From 535953ed1a968af30dac4776a8c908236f468c86 Mon Sep 17 00:00:00 2001 From: Raffaele Carelle Date: Thu, 13 Feb 2025 14:50:49 +0100 Subject: [PATCH 40/63] Enable `JSON_PRESERVE_ZERO_FRACTION` in `jsonRequest` method --- src/Symfony/Component/BrowserKit/AbstractBrowser.php | 2 +- .../Component/BrowserKit/Tests/AbstractBrowserTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/BrowserKit/AbstractBrowser.php b/src/Symfony/Component/BrowserKit/AbstractBrowser.php index d2a1faead4f67..37ab49d0cb7d1 100644 --- a/src/Symfony/Component/BrowserKit/AbstractBrowser.php +++ b/src/Symfony/Component/BrowserKit/AbstractBrowser.php @@ -170,7 +170,7 @@ public function xmlHttpRequest(string $method, string $uri, array $parameters = */ public function jsonRequest(string $method, string $uri, array $parameters = [], array $server = [], bool $changeHistory = true): Crawler { - $content = json_encode($parameters); + $content = json_encode($parameters, \JSON_PRESERVE_ZERO_FRACTION); $this->setServerParameter('CONTENT_TYPE', 'application/json'); $this->setServerParameter('HTTP_ACCEPT', 'application/json'); diff --git a/src/Symfony/Component/BrowserKit/Tests/AbstractBrowserTest.php b/src/Symfony/Component/BrowserKit/Tests/AbstractBrowserTest.php index 2267fca448799..504cc95878ef2 100644 --- a/src/Symfony/Component/BrowserKit/Tests/AbstractBrowserTest.php +++ b/src/Symfony/Component/BrowserKit/Tests/AbstractBrowserTest.php @@ -67,12 +67,12 @@ public function testXmlHttpRequest() public function testJsonRequest() { $client = $this->getBrowser(); - $client->jsonRequest('GET', 'http://example.com/', ['param' => 1], [], true); + $client->jsonRequest('GET', 'http://example.com/', ['param' => 1, 'float' => 10.0], [], true); $this->assertSame('application/json', $client->getRequest()->getServer()['CONTENT_TYPE']); $this->assertSame('application/json', $client->getRequest()->getServer()['HTTP_ACCEPT']); $this->assertFalse($client->getServerParameter('CONTENT_TYPE', false)); $this->assertFalse($client->getServerParameter('HTTP_ACCEPT', false)); - $this->assertSame('{"param":1}', $client->getRequest()->getContent()); + $this->assertSame('{"param":1,"float":10.0}', $client->getRequest()->getContent()); } public function testGetRequestWithIpAsHttpHost() From d4e8a5cf3f4a318131b3a778cb71b37efdf18f0f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 14 Feb 2025 13:21:59 +0100 Subject: [PATCH 41/63] fix rendering notifier message options --- .../Resources/views/Collector/notifier.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/notifier.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/notifier.html.twig index 9de8d216e6d1f..ed363f1d92fe2 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/notifier.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/notifier.html.twig @@ -151,7 +151,7 @@ {%- if message.getOptions() is null %} {{- '(empty)' }} {%- else %} - {{- message.getOptions()|json_encode(constant('JSON_PRETTY_PRINT')) }} + {{- message.getOptions().toArray()|json_encode(constant('JSON_PRETTY_PRINT')) }} {%- endif %} From 9a98452471f18a8c28f3bb0e2bb2b3182a204b7e Mon Sep 17 00:00:00 2001 From: Tom Kaminski Date: Fri, 14 Feb 2025 09:16:52 -0600 Subject: [PATCH 42/63] Check for null parent Nodes in the case of orphaned branches --- src/Symfony/Component/DomCrawler/Crawler.php | 2 +- .../Tests/AbstractCrawlerTestCase.php | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index ac5a9833842ff..005a69319263e 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -431,7 +431,7 @@ public function closest(string $selector): ?self $domNode = $this->getNode(0); - while (\XML_ELEMENT_NODE === $domNode->nodeType) { + while (null !== $domNode && \XML_ELEMENT_NODE === $domNode->nodeType) { $node = $this->createSubCrawler($domNode); if ($node->matches($selector)) { return $node; diff --git a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php index 97b16b9fe6073..5cdbbbf45870d 100644 --- a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php +++ b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php @@ -1031,6 +1031,29 @@ public function testClosest() $this->assertNull($notFound); } + public function testClosestWithOrphanedNode() + { + $html = <<<'HTML' + + +
+
+
+ + +HTML; + + $crawler = $this->createCrawler($this->getDoctype().$html); + $foo = $crawler->filter('#foo'); + + $fooNode = $foo->getNode(0); + + $fooNode->parentNode->replaceChild($fooNode->ownerDocument->createElement('ol'), $fooNode); + + $body = $foo->closest('body'); + $this->assertNull($body); + } + public function testOuterHtml() { $html = <<<'HTML' From 07e67881b55a3aad72d7c7d9d862d7a114eb998a Mon Sep 17 00:00:00 2001 From: DemigodCode Date: Fri, 14 Feb 2025 23:53:43 +0100 Subject: [PATCH 43/63] fix integration tests --- .github/workflows/integration-tests.yml | 21 ++++-- .../PredisRedisReplicationAdapterTest.php | 2 +- .../Adapter/PredisReplicationAdapterTest.php | 18 ++++- .../PredisTagAwareReplicationAdapterTest.php | 37 ----------- .../Adapter/RedisReplicationAdapterTest.php | 65 ------------------- 5 files changed, 33 insertions(+), 110 deletions(-) delete mode 100644 src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php delete mode 100644 src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 5c2839d74f818..9ea7e0992d939 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -82,16 +82,25 @@ jobs: REDIS_MASTER_SET: redis_sentinel REDIS_SENTINEL_QUORUM: 1 redis-primary: - image: redis:latest - hostname: redis-primary + image: bitnami/redis:latest ports: - 16381:6379 - + env: + ALLOW_EMPTY_PASSWORD: "yes" + REDIS_REPLICATION_MODE: "master" + options: >- + --name=redis-primary redis-replica: - image: redis:latest + image: bitnami/redis:latest ports: - 16382:6379 - command: redis-server --slaveof redis-primary 6379 + env: + ALLOW_EMPTY_PASSWORD: "yes" + REDIS_REPLICATION_MODE: "slave" + REDIS_MASTER_HOST: redis-primary + REDIS_MASTER_PORT_NUMBER: "6379" + options: >- + --name=redis-replica memcached: image: memcached:1.6.5 ports: @@ -250,7 +259,7 @@ jobs: REDIS_CLUSTER_HOSTS: 'localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005' REDIS_SENTINEL_HOSTS: 'unreachable-host:26379 localhost:26379 localhost:26379' REDIS_SENTINEL_SERVICE: redis_sentinel - REDIS_REPLICATION_HOSTS: 'localhost:16381 localhost:16382' + REDIS_REPLICATION_HOSTS: 'localhost:16382 localhost:16381' MESSENGER_REDIS_DSN: redis://127.0.0.1:7006/messages MESSENGER_AMQP_DSN: amqp://localhost/%2f/messages MESSENGER_SQS_DSN: "sqs://localhost:4566/messages?sslmode=disable&poll_timeout=0.01" diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php index 552727740c18b..cda92af8c7a6c 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php @@ -24,6 +24,6 @@ public static function setUpBeforeClass(): void self::markTestSkipped('REDIS_REPLICATION_HOSTS env var is not defined.'); } - self::$redis = RedisAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).'][alias]=master', ['class' => \Predis\Client::class, 'prefix' => 'prefix_']); + self::$redis = RedisAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).'][role]=master', ['replication' => 'predis', 'class' => \Predis\Client::class, 'prefix' => 'prefix_']); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php index 4add9d5f18c4a..28af1b5b4e27e 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php @@ -19,6 +19,22 @@ class PredisReplicationAdapterTest extends AbstractRedisAdapterTestCase public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - self::$redis = new \Predis\Client(array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => 6379]), ['prefix' => 'prefix_']); + + if (!$hosts = getenv('REDIS_REPLICATION_HOSTS')) { + self::markTestSkipped('REDIS_REPLICATION_HOSTS env var is not defined.'); + } + + $hosts = explode(' ', getenv('REDIS_REPLICATION_HOSTS')); + $lastArrayKey = array_key_last($hosts); + $hostTable = []; + foreach($hosts as $key => $host) { + $hostInformation = array_combine(['host', 'port'], explode(':', $host)); + if($lastArrayKey === $key) { + $hostInformation['role'] = 'master'; + } + $hostTable[] = $hostInformation; + } + + self::$redis = new \Predis\Client($hostTable, ['replication' => 'predis', 'prefix' => 'prefix_']); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php deleted file mode 100644 index 4d8651ce4ceb6..0000000000000 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Cache\Tests\Adapter; - -use Psr\Cache\CacheItemPoolInterface; -use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; - -/** - * @group integration - */ -class PredisTagAwareReplicationAdapterTest extends PredisReplicationAdapterTest -{ - use TagAwareTestTrait; - - protected function setUp(): void - { - parent::setUp(); - $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; - } - - public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface - { - $this->assertInstanceOf(\Predis\Client::class, self::$redis); - $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); - - return $adapter; - } -} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php deleted file mode 100644 index e41745057f141..0000000000000 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Cache\Tests\Adapter; - -use Psr\Cache\CacheItemPoolInterface; -use Symfony\Component\Cache\Adapter\AbstractAdapter; -use Symfony\Component\Cache\Adapter\RedisAdapter; -use Symfony\Component\Cache\Exception\InvalidArgumentException; -use Symfony\Component\Cache\Traits\RedisClusterProxy; - -/** - * @group integration - */ -class RedisReplicationAdapterTest extends AbstractRedisAdapterTestCase -{ - public static function setUpBeforeClass(): void - { - if (!$hosts = getenv('REDIS_REPLICATION_HOSTS')) { - self::markTestSkipped('REDIS_REPLICATION_HOSTS env var is not defined.'); - } - - self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).'][alias]=master', ['lazy' => true]); - self::$redis->setOption(\Redis::OPT_PREFIX, 'prefix_'); - } - - public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface - { - if ('testClearWithPrefix' === $testMethod && \defined('Redis::SCAN_PREFIX')) { - self::$redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_PREFIX); - } - - $this->assertInstanceOf(RedisClusterProxy::class, self::$redis); - $adapter = new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); - - return $adapter; - } - - /** - * @dataProvider provideFailedCreateConnection - */ - public function testFailedCreateConnection(string $dsn) - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Redis connection '); - RedisAdapter::createConnection($dsn); - } - - public static function provideFailedCreateConnection(): array - { - return [ - ['redis://localhost:1234'], - ['redis://foo@localhost?role=master'], - ['redis://localhost/123?role=master'], - ]; - } -} From 0d4a4982e7307979485c06639187087a0fa87852 Mon Sep 17 00:00:00 2001 From: tinect Date: Mon, 17 Feb 2025 22:23:52 +0100 Subject: [PATCH 44/63] [MIME] use address for body at PathHeader --- src/Symfony/Component/Mime/Header/PathHeader.php | 2 +- src/Symfony/Component/Mime/Tests/Header/HeadersTest.php | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Mime/Header/PathHeader.php b/src/Symfony/Component/Mime/Header/PathHeader.php index 63eb30af01bf8..4b0b7d3955673 100644 --- a/src/Symfony/Component/Mime/Header/PathHeader.php +++ b/src/Symfony/Component/Mime/Header/PathHeader.php @@ -57,6 +57,6 @@ public function getAddress(): Address public function getBodyAsString(): string { - return '<'.$this->address->toString().'>'; + return '<'.$this->address->getEncodedAddress().'>'; } } diff --git a/src/Symfony/Component/Mime/Tests/Header/HeadersTest.php b/src/Symfony/Component/Mime/Tests/Header/HeadersTest.php index b1892978c1a0b..b0a28fdbf992e 100644 --- a/src/Symfony/Component/Mime/Tests/Header/HeadersTest.php +++ b/src/Symfony/Component/Mime/Tests/Header/HeadersTest.php @@ -346,4 +346,12 @@ public function testSetHeaderParameterNotParameterized() $this->expectException(\LogicException::class); $headers->setHeaderParameter('Content-Disposition', 'name', 'foo'); } + + public function testPathHeaderHasNoName() + { + $headers = new Headers(); + + $headers->addPathHeader('Return-Path', new Address('some@path', 'any ignored name')); + $this->assertSame('', $headers->get('Return-Path')->getBodyAsString()); + } } From b1d5de50bd6cbf24636570017fab5e599677ca83 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 18 Feb 2025 09:43:25 +0100 Subject: [PATCH 45/63] [DoctrineBridge] Fix deprecation with `doctrine/dbal` ^4.3 --- .../Doctrine/Tests/Fixtures/SingleAssociationToIntIdEntity.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleAssociationToIntIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleAssociationToIntIdEntity.php index 94becf73b5795..0373417b2c8bb 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleAssociationToIntIdEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleAssociationToIntIdEntity.php @@ -14,6 +14,7 @@ use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Mapping\OneToOne; #[Entity] @@ -21,6 +22,7 @@ class SingleAssociationToIntIdEntity { public function __construct( #[Id, OneToOne(cascade: ['ALL'])] + #[JoinColumn(nullable: false)] protected SingleIntIdNoToStringEntity $entity, #[Column(nullable: true)] From e71a278737e54dfa6efa25e0344808e47eec4123 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 18 Feb 2025 13:36:18 +0100 Subject: [PATCH 46/63] [Validator] Fix incorrect assertion in `WhenTest` --- .../Component/Validator/Tests/Constraints/WhenTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/WhenTest.php b/src/Symfony/Component/Validator/Tests/Constraints/WhenTest.php index 7e11f0f683746..de13876db6867 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/WhenTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/WhenTest.php @@ -89,7 +89,7 @@ public function testAnnotations() [$barConstraint] = $metadata->properties['bar']->getConstraints(); - self::assertInstanceOf(When::class, $fooConstraint); + self::assertInstanceOf(When::class, $barConstraint); self::assertSame('false', $barConstraint->expression); self::assertEquals([ new NotNull([ @@ -161,7 +161,7 @@ public function testAttributes() [$barConstraint] = $metadata->properties['bar']->getConstraints(); - self::assertInstanceOf(When::class, $fooConstraint); + self::assertInstanceOf(When::class, $barConstraint); self::assertSame('false', $barConstraint->expression); self::assertEquals([ new NotNull([ From 9c0213b0d23688c74c09b2d5586fde96c12afa64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20M=C3=B6nch?= Date: Tue, 18 Feb 2025 16:35:34 +0100 Subject: [PATCH 47/63] [Semaphore] allow redis cluster/sentinel dsn --- .../Semaphore/Store/StoreFactory.php | 4 +- .../Tests/Store/StoreFactoryTest.php | 49 ++++++------------- 2 files changed, 18 insertions(+), 35 deletions(-) diff --git a/src/Symfony/Component/Semaphore/Store/StoreFactory.php b/src/Symfony/Component/Semaphore/Store/StoreFactory.php index 639298bab2453..97fe89749e592 100644 --- a/src/Symfony/Component/Semaphore/Store/StoreFactory.php +++ b/src/Symfony/Component/Semaphore/Store/StoreFactory.php @@ -35,8 +35,8 @@ public static function createStore(#[\SensitiveParameter] object|string $connect case !\is_string($connection): throw new InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection::class)); - case str_starts_with($connection, 'redis://'): - case str_starts_with($connection, 'rediss://'): + case str_starts_with($connection, 'redis:'): + case str_starts_with($connection, 'rediss:'): if (!class_exists(AbstractAdapter::class)) { throw new InvalidArgumentException('Unsupported Redis DSN. Try running "composer require symfony/cache".'); } diff --git a/src/Symfony/Component/Semaphore/Tests/Store/StoreFactoryTest.php b/src/Symfony/Component/Semaphore/Tests/Store/StoreFactoryTest.php index 23d01346a8b0b..f69e7161e4677 100644 --- a/src/Symfony/Component/Semaphore/Tests/Store/StoreFactoryTest.php +++ b/src/Symfony/Component/Semaphore/Tests/Store/StoreFactoryTest.php @@ -12,54 +12,37 @@ namespace Symfony\Component\Semaphore\Tests\Store; use PHPUnit\Framework\TestCase; -use Symfony\Component\Cache\Traits\RedisProxy; use Symfony\Component\Semaphore\Store\RedisStore; use Symfony\Component\Semaphore\Store\StoreFactory; /** * @author Jérémy Derussé - * - * @requires extension redis */ class StoreFactoryTest extends TestCase { - public function testCreateRedisStore() + /** + * @dataProvider validConnections + */ + public function testCreateStore($connection, string $expectedStoreClass) { - $store = StoreFactory::createStore($this->createMock(\Redis::class)); + $store = StoreFactory::createStore($connection); - $this->assertInstanceOf(RedisStore::class, $store); + $this->assertInstanceOf($expectedStoreClass, $store); } - public function testCreateRedisProxyStore() + public static function validConnections(): \Generator { - if (!class_exists(RedisProxy::class)) { - $this->markTestSkipped(); - } + yield [new \Predis\Client(), RedisStore::class]; - $store = StoreFactory::createStore($this->createMock(RedisProxy::class)); - - $this->assertInstanceOf(RedisStore::class, $store); - } - - public function testCreateRedisAsDsnStore() - { - if (!class_exists(RedisProxy::class)) { - $this->markTestSkipped(); + if (class_exists(\Redis::class)) { + yield [new \Redis(), RedisStore::class]; } - - $store = StoreFactory::createStore('redis://localhost'); - - $this->assertInstanceOf(RedisStore::class, $store); - } - - public function testCreatePredisStore() - { - if (!class_exists(\Predis\Client::class)) { - $this->markTestSkipped(); + if (class_exists(\Redis::class) && class_exists(AbstractAdapter::class)) { + yield ['redis://localhost', RedisStore::class]; + yield ['redis://localhost?lazy=1', RedisStore::class]; + yield ['redis://localhost?redis_cluster=1', RedisStore::class]; + yield ['redis://localhost?redis_cluster=1&lazy=1', RedisStore::class]; + yield ['redis:?host[localhost]&host[localhost:6379]&redis_cluster=1', RedisStore::class]; } - - $store = StoreFactory::createStore(new \Predis\Client()); - - $this->assertInstanceOf(RedisStore::class, $store); } } From b188dccd491e150b5247b5add6a91dcb2c76287d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 18 Feb 2025 17:53:06 +0100 Subject: [PATCH 48/63] [psalm] ensureOverrideAttribute="false" --- psalm.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/psalm.xml b/psalm.xml index a21be22fe248f..86491b32709c7 100644 --- a/psalm.xml +++ b/psalm.xml @@ -10,6 +10,7 @@ findUnusedBaselineEntry="false" findUnusedCode="false" findUnusedIssueHandlerSuppression="false" + ensureOverrideAttribute="false" > From 5c2ebfba1ce08d9e5d562fde9a6bd6aa8492bd0c Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 19 Feb 2025 14:12:02 +0100 Subject: [PATCH 49/63] [Validator] Synchronize IBAN formats --- .../Component/Validator/Constraints/IbanValidator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/IbanValidator.php b/src/Symfony/Component/Validator/Constraints/IbanValidator.php index 11619efd8ec7a..13d91315b5dea 100644 --- a/src/Symfony/Component/Validator/Constraints/IbanValidator.php +++ b/src/Symfony/Component/Validator/Constraints/IbanValidator.php @@ -69,7 +69,7 @@ class IbanValidator extends ConstraintValidator 'DK' => 'DK\d{2}\d{4}\d{9}\d{1}', // Denmark 'DO' => 'DO\d{2}[\dA-Z]{4}\d{20}', // Dominican Republic 'DZ' => 'DZ\d{2}\d{22}', // Algeria - 'EE' => 'EE\d{2}\d{2}\d{2}\d{11}\d{1}', // Estonia + 'EE' => 'EE\d{2}\d{2}\d{14}', // Estonia 'EG' => 'EG\d{2}\d{4}\d{4}\d{17}', // Egypt 'ES' => 'ES\d{2}\d{4}\d{4}\d{1}\d{1}\d{10}', // Spain 'FI' => 'FI\d{2}\d{3}\d{11}', // Finland @@ -126,7 +126,7 @@ class IbanValidator extends ConstraintValidator 'MZ' => 'MZ\d{2}\d{21}', // Mozambique 'NC' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'NE' => 'NE\d{2}[A-Z]{2}\d{22}', // Niger - 'NI' => 'NI\d{2}[A-Z]{4}\d{24}', // Nicaragua + 'NI' => 'NI\d{2}[A-Z]{4}\d{20}', // Nicaragua 'NL' => 'NL\d{2}[A-Z]{4}\d{10}', // Netherlands (The) 'NO' => 'NO\d{2}\d{4}\d{6}\d{1}', // Norway 'OM' => 'OM\d{2}\d{3}[\dA-Z]{16}', // Oman @@ -150,7 +150,7 @@ class IbanValidator extends ConstraintValidator 'SM' => 'SM\d{2}[A-Z]{1}\d{5}\d{5}[\dA-Z]{12}', // San Marino 'SN' => 'SN\d{2}[A-Z]{2}\d{22}', // Senegal 'SO' => 'SO\d{2}\d{4}\d{3}\d{12}', // Somalia - 'ST' => 'ST\d{2}\d{4}\d{4}\d{11}\d{2}', // Sao Tome and Principe + 'ST' => 'ST\d{2}\d{8}\d{11}\d{2}', // Sao Tome and Principe 'SV' => 'SV\d{2}[A-Z]{4}\d{20}', // El Salvador 'TD' => 'TD\d{2}\d{23}', // Chad 'TF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France From 124087b405520e7446dd79426035e16a099f6b6d Mon Sep 17 00:00:00 2001 From: Artem Lopata Date: Wed, 19 Feb 2025 12:07:11 +0100 Subject: [PATCH 50/63] [DependencyInjection] Defer check for circular references instead of skipping them. --- .../Compiler/CheckCircularReferencesPass.php | 35 +++++++++++++------ .../CheckCircularReferencesPassTest.php | 18 ++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php index 1fb8935c3e102..a4a8ce368e51d 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php @@ -28,6 +28,7 @@ class CheckCircularReferencesPass implements CompilerPassInterface { private array $currentPath; private array $checkedNodes; + private array $checkedLazyNodes; /** * Checks the ContainerBuilder object for circular references. @@ -59,22 +60,36 @@ private function checkOutEdges(array $edges): void $node = $edge->getDestNode(); $id = $node->getId(); - if (empty($this->checkedNodes[$id])) { - // Don't check circular references for lazy edges - if (!$node->getValue() || (!$edge->isLazy() && !$edge->isWeak())) { - $searchKey = array_search($id, $this->currentPath); - $this->currentPath[] = $id; + if (!empty($this->checkedNodes[$id])) { + continue; + } + + $isLeaf = !!$node->getValue(); + $isConcrete = !$edge->isLazy() && !$edge->isWeak(); + + // Skip already checked lazy services if they are still lazy. Will not gain any new information. + if (!empty($this->checkedLazyNodes[$id]) && (!$isLeaf || !$isConcrete)) { + continue; + } - if (false !== $searchKey) { - throw new ServiceCircularReferenceException($id, \array_slice($this->currentPath, $searchKey)); - } + // Process concrete references, otherwise defer check circular references for lazy edges. + if (!$isLeaf || $isConcrete) { + $searchKey = array_search($id, $this->currentPath); + $this->currentPath[] = $id; - $this->checkOutEdges($node->getOutEdges()); + if (false !== $searchKey) { + throw new ServiceCircularReferenceException($id, \array_slice($this->currentPath, $searchKey)); } + $this->checkOutEdges($node->getOutEdges()); + $this->checkedNodes[$id] = true; - array_pop($this->currentPath); + unset($this->checkedLazyNodes[$id]); + } else { + $this->checkedLazyNodes[$id] = true; } + + array_pop($this->currentPath); } } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckCircularReferencesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckCircularReferencesPassTest.php index c9bcb10878bec..20a0a7b5a8d5a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckCircularReferencesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckCircularReferencesPassTest.php @@ -13,9 +13,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; +use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass; use Symfony\Component\DependencyInjection\Compiler\CheckCircularReferencesPass; use Symfony\Component\DependencyInjection\Compiler\Compiler; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\Reference; @@ -126,6 +129,21 @@ public function testProcessIgnoresLazyServices() $this->addToAssertionCount(1); } + public function testProcessDefersLazyServices() + { + $container = new ContainerBuilder(); + + $container->register('a')->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('tag', needsIndexes: true))); + $container->register('b')->addArgument(new Reference('c'))->addTag('tag'); + $container->register('c')->addArgument(new Reference('b')); + + (new ServiceLocatorTagPass())->process($container); + + $this->expectException(ServiceCircularReferenceException::class); + + $this->process($container); + } + public function testProcessIgnoresIteratorArguments() { $container = new ContainerBuilder(); From a5f779ca417619d9cd598cba46638b64cd3e6c77 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Thu, 20 Feb 2025 12:25:33 +0100 Subject: [PATCH 51/63] [TypeInfo] Fix create union with nullable type --- src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php | 4 ++++ src/Symfony/Component/TypeInfo/TypeFactoryTrait.php | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php index 60a0ded22c648..50a6f5a2e6449 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php @@ -205,5 +205,9 @@ public function testCreateNullable() new NullableType(new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING))), Type::nullable(Type::union(Type::int(), Type::string(), Type::null())), ); + $this->assertEquals( + new NullableType(new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING))), + Type::union(Type::nullable(Type::int()), Type::string()), + ); } } diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php index d32a97276057c..b8e15f209fa00 100644 --- a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php +++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php @@ -266,6 +266,13 @@ public static function union(Type ...$types): UnionType $isNullable = fn (Type $type): bool => $type instanceof BuiltinType && TypeIdentifier::NULL === $type->getTypeIdentifier(); foreach ($types as $type) { + if ($type instanceof NullableType) { + $nullableUnion = true; + $unionTypes[] = $type->getWrappedType(); + + continue; + } + if ($type instanceof UnionType) { foreach ($type->getTypes() as $unionType) { if ($isNullable($type)) { From ce035629a4466c225a220635eb0c95707e0797d1 Mon Sep 17 00:00:00 2001 From: Joseph FRANCLIN Date: Thu, 20 Feb 2025 19:28:31 +0100 Subject: [PATCH 52/63] [DependencyInjection] Fix phpdoc for $configurator in Autoconfigure attribute --- .../Component/DependencyInjection/Attribute/Autoconfigure.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php b/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php index dc2c84ca29a5e..06513fd903e01 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php @@ -28,7 +28,7 @@ class Autoconfigure * @param bool|null $shared Whether to declare the service as shared * @param bool|null $autowire Whether to declare the service as autowired * @param array|null $properties The properties to define when creating the service - * @param array|string|null $configurator A PHP function, reference or an array containing a class/Reference and a method to call after the service is fully initialized + * @param array{string, string}|string|null $configurator A PHP function, reference or an array containing a class/reference and a method to call after the service is fully initialized * @param string|null $constructor The public static method to use to instantiate the service */ public function __construct( From e737911a9c43e38da4d07aeeb76e4de529c2986f Mon Sep 17 00:00:00 2001 From: Alexander Dmitryuk Date: Fri, 21 Feb 2025 09:36:22 +0000 Subject: [PATCH 53/63] [Stopwatch] Fix StopWatchEvent never throws InvalidArgumentException --- src/Symfony/Component/Stopwatch/StopwatchEvent.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Symfony/Component/Stopwatch/StopwatchEvent.php b/src/Symfony/Component/Stopwatch/StopwatchEvent.php index 1a85d80cae92f..782307b5a5e8f 100644 --- a/src/Symfony/Component/Stopwatch/StopwatchEvent.php +++ b/src/Symfony/Component/Stopwatch/StopwatchEvent.php @@ -39,8 +39,6 @@ class StopwatchEvent * @param string|null $category The event category or null to use the default * @param bool $morePrecision If true, time is stored as float to keep the original microsecond precision * @param string|null $name The event name or null to define the name as default - * - * @throws \InvalidArgumentException When the raw time is not valid */ public function __construct(float $origin, ?string $category = null, bool $morePrecision = false, ?string $name = null) { @@ -207,8 +205,6 @@ protected function getNow(): float /** * Formats a time. - * - * @throws \InvalidArgumentException When the raw time is not valid */ private function formatTime(float $time): float { From e73f3bb967393e0ab13d730c673c81e84656f445 Mon Sep 17 00:00:00 2001 From: Quentin Schuler Date: Fri, 21 Feb 2025 14:41:03 +0100 Subject: [PATCH 54/63] [FrameworkBundle] Disable the keys normalization of the CSRF form field attributes The form.csrf_protection.field_attr configuration node value should remain as-is when defined. The default behavior of the configuration component is to normalize keys, but in that specific cases, keys becomes HTML attributes and therefore should not be changed. This commit fix that behaviour for the specific node. --- .../DependencyInjection/Configuration.php | 1 + .../DependencyInjection/ConfigurationTest.php | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 678698f4d0747..4d494eed09e60 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -250,6 +250,7 @@ private function addFormSection(ArrayNodeDefinition $rootNode, callable $enableI ->scalarNode('field_name')->defaultValue('_token')->end() ->arrayNode('field_attr') ->performNoDeepMerging() + ->normalizeKeys(false) ->scalarPrototype()->end() ->defaultValue(['data-controller' => 'csrf-protection']) ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 53706d2e05e32..6f3363f3998a0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -699,6 +699,22 @@ public function testSerializerJsonDetailedErrorMessagesNotSetByDefaultWithDebugD $this->assertSame([], $config['serializer']['default_context'] ?? []); } + public function testFormCsrfProtectionFieldAttrDoNotNormalizeKeys() + { + $processor = new Processor(); + $config = $processor->processConfiguration(new Configuration(false), [ + [ + 'form' => [ + 'csrf_protection' => [ + 'field_attr' => ['data-example-attr' => 'value'], + ], + ], + ], + ]); + + $this->assertSame(['data-example-attr' => 'value'], $config['form']['csrf_protection']['field_attr'] ?? []); + } + protected static function getBundleDefaultConfig() { return [ From 04ff18d41957935efbd259e5539cc0bdcb1b6ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Hlavat=C3=BD?= <107676055+Pepperoni1337@users.noreply.github.com> Date: Sun, 23 Feb 2025 11:21:59 +0100 Subject: [PATCH 55/63] Update GetSetMethodNormalizer.php Fix: Add length check for setter method detection --- .../Component/Serializer/Normalizer/GetSetMethodNormalizer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index 951005545f5e9..3cb9b992bd8db 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -118,6 +118,7 @@ private function isSetMethod(\ReflectionMethod $method): bool return !$method->isStatic() && !$method->getAttributes(Ignore::class) && 0 < $method->getNumberOfParameters() + && 3 < \strlen($method->name) && str_starts_with($method->name, 'set') && !ctype_lower($method->name[3]) ; From 00525422a2742e239823701918fdff6c72ccf84a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 24 Feb 2025 11:00:12 +0100 Subject: [PATCH 56/63] skip failing Semaphore component tests on GitHub Actions with PHP 8.5 --- .../Component/Semaphore/Tests/Store/RelayStoreTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Symfony/Component/Semaphore/Tests/Store/RelayStoreTest.php b/src/Symfony/Component/Semaphore/Tests/Store/RelayStoreTest.php index a7db8f8f10cf1..aeaf8c3d451ce 100644 --- a/src/Symfony/Component/Semaphore/Tests/Store/RelayStoreTest.php +++ b/src/Symfony/Component/Semaphore/Tests/Store/RelayStoreTest.php @@ -25,6 +25,10 @@ protected function setUp(): void public static function setUpBeforeClass(): void { + if (\PHP_VERSION_ID <= 80500 && isset($_SERVER['GITHUB_ACTIONS'])) { + self::markTestSkipped('Test segfaults on PHP 8.5'); + } + try { new Relay(...explode(':', getenv('REDIS_HOST'))); } catch (\Relay\Exception $e) { From 9ea45d0ec95acbe4cc31a5b0ff90df966e80ddd7 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 24 Feb 2025 12:15:37 +0100 Subject: [PATCH 57/63] rewrite tests to not fail when self/parent are resolved at compile time --- .../ReflectionExtractableDummyUsingTrait.php | 17 +++++++++ .../Fixtures/ReflectionExtractableTrait.php | 27 ++++++++++++++ .../ReflectionParameterTypeResolverTest.php | 22 +++++++++-- .../ReflectionPropertyTypeResolverTest.php | 22 +++++++++-- .../ReflectionReturnTypeResolverTest.php | 22 +++++++++-- .../ReflectionTypeResolverTest.php | 37 ++++++++++++------- 6 files changed, 125 insertions(+), 22 deletions(-) create mode 100644 src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummyUsingTrait.php create mode 100644 src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableTrait.php diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummyUsingTrait.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummyUsingTrait.php new file mode 100644 index 0000000000000..77fb0b02966b7 --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummyUsingTrait.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\TypeInfo\Tests\Fixtures; + +class ReflectionExtractableDummyUsingTrait +{ + use ReflectionExtractableTrait; +} diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableTrait.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableTrait.php new file mode 100644 index 0000000000000..5bc33e0bbd315 --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableTrait.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\TypeInfo\Tests\Fixtures; + +trait ReflectionExtractableTrait +{ + public self $self; + + public function getSelf(): self + { + return $this; + } + + public function setSelf(self $self): void + { + $this->self = $self; + } +} diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionParameterTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionParameterTypeResolverTest.php index 41a46a899751e..e70106088db48 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionParameterTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionParameterTypeResolverTest.php @@ -14,6 +14,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\TypeInfo\Exception\UnsupportedException; use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableDummy; +use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableDummyUsingTrait; +use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableTrait; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; use Symfony\Component\TypeInfo\TypeResolver\ReflectionParameterTypeResolver; @@ -71,15 +73,29 @@ public function testResolveOptionalParameter() $this->assertEquals(Type::nullable(Type::int()), $this->resolver->resolve($reflectionParameter)); } - public function testCreateTypeContextOrUseProvided() + public function testResolveSelfFromClassWithoutContext() { $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class); $reflectionParameter = $reflectionClass->getMethod('setSelf')->getParameters()[0]; $this->assertEquals(Type::object(ReflectionExtractableDummy::class), $this->resolver->resolve($reflectionParameter)); + } + + public function testResolveSelfFromTraitWithoutContext() + { + $reflectionClass = new \ReflectionClass(ReflectionExtractableTrait::class); + $reflectionParameter = $reflectionClass->getMethod('setSelf')->getParameters()[0]; + + $this->assertEquals(Type::object(ReflectionExtractableTrait::class), $this->resolver->resolve($reflectionParameter)); + } + + public function testResolveSelfFromTraitWithClassContext() + { + $reflectionClass = new \ReflectionClass(ReflectionExtractableTrait::class); + $reflectionParameter = $reflectionClass->getMethod('setSelf')->getParameters()[0]; - $typeContext = (new TypeContextFactory())->createFromClassName(self::class); + $typeContext = (new TypeContextFactory())->createFromClassName(ReflectionExtractableDummyUsingTrait::class); - $this->assertEquals(Type::object(self::class), $this->resolver->resolve($reflectionParameter, $typeContext)); + $this->assertEquals(Type::object(ReflectionExtractableDummyUsingTrait::class), $this->resolver->resolve($reflectionParameter, $typeContext)); } } diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionPropertyTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionPropertyTypeResolverTest.php index 6935f818b6f17..1c756884e697f 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionPropertyTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionPropertyTypeResolverTest.php @@ -14,6 +14,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\TypeInfo\Exception\UnsupportedException; use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableDummy; +use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableDummyUsingTrait; +use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableTrait; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; use Symfony\Component\TypeInfo\TypeResolver\ReflectionPropertyTypeResolver; @@ -52,15 +54,29 @@ public function testResolve() $this->assertEquals(Type::int(), $this->resolver->resolve($reflectionProperty)); } - public function testCreateTypeContextOrUseProvided() + public function testResolveSelfFromClassWithoutContext() { $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class); $reflectionProperty = $reflectionClass->getProperty('self'); $this->assertEquals(Type::object(ReflectionExtractableDummy::class), $this->resolver->resolve($reflectionProperty)); + } + + public function testResolveSelfFromTraitWithoutContext() + { + $reflectionClass = new \ReflectionClass(ReflectionExtractableTrait::class); + $reflectionProperty = $reflectionClass->getProperty('self'); + + $this->assertEquals(Type::object(ReflectionExtractableTrait::class), $this->resolver->resolve($reflectionProperty)); + } + + public function testResolveSelfFromTraitWithClassContext() + { + $reflectionClass = new \ReflectionClass(ReflectionExtractableTrait::class); + $reflectionProperty = $reflectionClass->getProperty('self'); - $typeContext = (new TypeContextFactory())->createFromClassName(self::class); + $typeContext = (new TypeContextFactory())->createFromClassName(ReflectionExtractableDummyUsingTrait::class); - $this->assertEquals(Type::object(self::class), $this->resolver->resolve($reflectionProperty, $typeContext)); + $this->assertEquals(Type::object(ReflectionExtractableDummyUsingTrait::class), $this->resolver->resolve($reflectionProperty, $typeContext)); } } diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionReturnTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionReturnTypeResolverTest.php index 691a7d710af8c..13bfa2561fc37 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionReturnTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionReturnTypeResolverTest.php @@ -14,6 +14,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\TypeInfo\Exception\UnsupportedException; use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableDummy; +use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableDummyUsingTrait; +use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableTrait; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; use Symfony\Component\TypeInfo\TypeResolver\ReflectionReturnTypeResolver; @@ -62,15 +64,29 @@ public function testResolve() $this->assertEquals(Type::int(), $this->resolver->resolve($reflectionFunction)); } - public function testCreateTypeContextOrUseProvided() + public function testResolveSelfFromClassWithoutContext() { $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class); $reflectionFunction = $reflectionClass->getMethod('getSelf'); $this->assertEquals(Type::object(ReflectionExtractableDummy::class), $this->resolver->resolve($reflectionFunction)); + } + + public function testResolveSelfFromTraitWithoutContext() + { + $reflectionClass = new \ReflectionClass(ReflectionExtractableTrait::class); + $reflectionFunction = $reflectionClass->getMethod('getSelf'); + + $this->assertEquals(Type::object(ReflectionExtractableTrait::class), $this->resolver->resolve($reflectionFunction)); + } + + public function testResolveSelfFromTraitWithClassContext() + { + $reflectionClass = new \ReflectionClass(ReflectionExtractableTrait::class); + $reflectionFunction = $reflectionClass->getMethod('getSelf'); - $typeContext = (new TypeContextFactory())->createFromClassName(self::class); + $typeContext = (new TypeContextFactory())->createFromClassName(ReflectionExtractableDummyUsingTrait::class); - $this->assertEquals(Type::object(self::class), $this->resolver->resolve($reflectionFunction, $typeContext)); + $this->assertEquals(Type::object(ReflectionExtractableDummyUsingTrait::class), $this->resolver->resolve($reflectionFunction, $typeContext)); } } diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php index 4cadd5943e102..75116d97c2c3d 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php @@ -77,24 +77,35 @@ public function testCannotResolveNonProperReflectionType() $this->resolver->resolve(new \ReflectionClass(self::class)); } - /** - * @dataProvider classKeywordsTypesDataProvider - */ - public function testCannotResolveClassKeywordsWithoutTypeContext(\ReflectionType $reflection) + public function testCannotResolveStaticKeywordWithoutTypeContext() { + $subject = (new \ReflectionClass(ReflectionExtractableDummy::class))->getMethod('getStatic')->getReturnType(); + $this->expectException(InvalidArgumentException::class); - $this->resolver->resolve($reflection); + $this->resolver->resolve($subject); } - /** - * @return iterable - */ - public static function classKeywordsTypesDataProvider(): iterable + public function testResolveSelfKeywordWithoutTypeContext() { - $reflection = new \ReflectionClass(ReflectionExtractableDummy::class); + $subject = (new \ReflectionClass(ReflectionExtractableDummy::class))->getProperty('self')->getType(); + + if (\PHP_VERSION_ID >= 80500) { + $this->assertEquals(Type::object(ReflectionExtractableDummy::class), $this->resolver->resolve($subject)); + } else { + $this->expectException(InvalidArgumentException::class); + $this->resolver->resolve($subject); + } + } + + public function testResolveParentKeywordsWithoutTypeContext() + { + $subject = (new \ReflectionClass(ReflectionExtractableDummy::class))->getProperty('parent')->getType(); - yield [$reflection->getProperty('self')->getType()]; - yield [$reflection->getMethod('getStatic')->getReturnType()]; - yield [$reflection->getProperty('parent')->getType()]; + if (\PHP_VERSION_ID >= 80500) { + $this->assertEquals(Type::object(AbstractDummy::class), $this->resolver->resolve($subject)); + } else { + $this->expectException(InvalidArgumentException::class); + $this->resolver->resolve($subject); + } } } From 83b0491341f219420b59744aa8cad106ff867058 Mon Sep 17 00:00:00 2001 From: iraouf Date: Sun, 23 Feb 2025 01:24:22 +0100 Subject: [PATCH 58/63] [Mailer][Postmark] Set CID for attachments when it exists --- .../Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php index ea5ac37671c12..1ed5e5c6bc644 100644 --- a/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php @@ -147,7 +147,7 @@ private function getAttachments(Email $email): array ]; if ('inline' === $disposition) { - $att['ContentID'] = 'cid:'.$filename; + $att['ContentID'] = 'cid:'.($attachment->hasContentId() ? $attachment->getContentId() : $filename); } $attachments[] = $att; From a50880b0675b941a945f388b82a90f77239e975d Mon Sep 17 00:00:00 2001 From: fabi Date: Fri, 14 Feb 2025 20:13:35 +0100 Subject: [PATCH 59/63] [Mailer] fix multiple transports default injection --- .../DependencyInjection/FrameworkExtension.php | 1 - .../Bundle/FrameworkBundle/Resources/config/mailer.php | 5 +---- .../Tests/DependencyInjection/FrameworkExtensionTestCase.php | 3 +-- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index f918eafbb209c..3a518bee7959e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2660,7 +2660,6 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co } $transports = $config['dsn'] ? ['main' => $config['dsn']] : $config['transports']; $container->getDefinition('mailer.transports')->setArgument(0, $transports); - $container->getDefinition('mailer.default_transport')->setArgument(0, current($transports)); $mailer = $container->getDefinition('mailer.mailer'); if (false === $messageBus = $config['message_bus']) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php index 9eb545ca268ea..7a3a95739b0f2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php @@ -46,10 +46,7 @@ ]) ->set('mailer.default_transport', TransportInterface::class) - ->factory([service('mailer.transport_factory'), 'fromString']) - ->args([ - abstract_arg('env(MAILER_DSN)'), - ]) + ->alias('mailer.default_transport', 'mailer.transports') ->alias(TransportInterface::class, 'mailer.default_transport') ->set('mailer.messenger.message_handler', MessageHandler::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index c891ec143fa13..7f94b83ce58c4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -2103,8 +2103,7 @@ public function testMailer(string $configFile, array $expectedTransports, array $this->assertTrue($container->hasAlias('mailer')); $this->assertTrue($container->hasDefinition('mailer.transports')); $this->assertSame($expectedTransports, $container->getDefinition('mailer.transports')->getArgument(0)); - $this->assertTrue($container->hasDefinition('mailer.default_transport')); - $this->assertSame(current($expectedTransports), $container->getDefinition('mailer.default_transport')->getArgument(0)); + $this->assertTrue($container->hasAlias('mailer.default_transport')); $this->assertTrue($container->hasDefinition('mailer.envelope_listener')); $l = $container->getDefinition('mailer.envelope_listener'); $this->assertSame('sender@example.org', $l->getArgument(0)); From 34c7e6f1902842bee641010916f138535800c73d Mon Sep 17 00:00:00 2001 From: Wolfgang Klinger Date: Thu, 12 Dec 2024 10:44:13 +0100 Subject: [PATCH 60/63] [Messenger] Filter out non-consumable receivers when registering `ConsumeMessagesCommand` --- .../FrameworkExtension.php | 12 +++++--- .../Command/ConsumeMessagesCommand.php | 6 ++++ .../DependencyInjection/MessengerPass.php | 6 +++- .../DependencyInjection/MessengerPassTest.php | 29 +++++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index f918eafbb209c..61f68c198c447 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2282,13 +2282,17 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $transportRateLimiterReferences = []; foreach ($config['transports'] as $name => $transport) { $serializerId = $transport['serializer'] ?? 'messenger.default_serializer'; + $tags = [ + 'alias' => $name, + 'is_failure_transport' => \in_array($name, $failureTransports), + ]; + if (str_starts_with($transport['dsn'], 'sync://')) { + $tags['is_consumable'] = false; + } $transportDefinition = (new Definition(TransportInterface::class)) ->setFactory([new Reference('messenger.transport_factory'), 'createTransport']) ->setArguments([$transport['dsn'], $transport['options'] + ['transport_name' => $name], new Reference($serializerId)]) - ->addTag('messenger.receiver', [ - 'alias' => $name, - 'is_failure_transport' => \in_array($name, $failureTransports), - ]) + ->addTag('messenger.receiver', $tags) ; $container->setDefinition($transportId = 'messenger.transport.'.$name, $transportDefinition); $senderAliases[$name] = $transportId; diff --git a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php index a959c2baee911..7aa8752f5616c 100644 --- a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php +++ b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php @@ -136,6 +136,12 @@ protected function interact(InputInterface $input, OutputInterface $output) $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); if ($this->receiverNames && !$input->getArgument('receivers')) { + if (1 === \count($this->receiverNames)) { + $input->setArgument('receivers', $this->receiverNames); + + return; + } + $io->block('Which transports/receivers do you want to consume?', null, 'fg=white;bg=blue', ' ', true); $io->writeln('Choose which receivers you want to consume messages from in order of priority.'); diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index 032ec76efa5e2..98ad205838bf9 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -274,6 +274,7 @@ private function registerReceivers(ContainerBuilder $container, array $busIds): } } + $consumableReceiverNames = []; foreach ($container->findTaggedServiceIds('messenger.receiver') as $id => $tags) { $receiverClass = $this->getServiceClass($container, $id); if (!is_subclass_of($receiverClass, ReceiverInterface::class)) { @@ -289,6 +290,9 @@ private function registerReceivers(ContainerBuilder $container, array $busIds): $failureTransportsMap[$tag['alias']] = $receiverMapping[$id]; } } + if (!isset($tag['is_consumable']) || $tag['is_consumable'] !== false) { + $consumableReceiverNames[] = $tag['alias'] ?? $id; + } } } @@ -314,7 +318,7 @@ private function registerReceivers(ContainerBuilder $container, array $busIds): $consumeCommandDefinition->replaceArgument(0, new Reference('messenger.routable_message_bus')); } - $consumeCommandDefinition->replaceArgument(4, array_values($receiverNames)); + $consumeCommandDefinition->replaceArgument(4, $consumableReceiverNames); try { $consumeCommandDefinition->replaceArgument(6, $busIds); } catch (OutOfBoundsException) { diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index 13d18993eb97c..e75117e5573b0 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Component\Console\Command\Command; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\AttributeAutoconfigurationPass; use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass; @@ -506,6 +507,34 @@ public function testItSetsTheReceiverNamesOnTheSetupTransportsCommand() $this->assertSame(['amqp', 'dummy'], $container->getDefinition('console.command.messenger_setup_transports')->getArgument(1)); } + public function testOnlyConsumableTransportsAreAddedToConsumeCommand() + { + $container = new ContainerBuilder(); + + $container->register('messenger.transport.async', DummyReceiver::class) + ->addTag('messenger.receiver', ['alias' => 'async']); + $container->register('messenger.transport.sync', DummyReceiver::class) + ->addTag('messenger.receiver', ['alias' => 'sync', 'is_consumable' => false]); + $container->register('messenger.receiver_locator', ServiceLocator::class) + ->setArguments([[]]); + + $container->register('console.command.messenger_consume_messages', Command::class) + ->setArguments([ + null, + null, + null, + null, + [], + ]); + + (new MessengerPass())->process($container); + + $this->assertSame( + ['async'], + $container->getDefinition('console.command.messenger_consume_messages')->getArgument(4) + ); + } + /** * @group legacy */ From 3686ba153e041f3b0361278ab02f1bd10b1a8e09 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 26 Feb 2025 09:12:21 +0100 Subject: [PATCH 61/63] [Cache] Fix `PredisAdapter` tests --- .../Cache/Tests/Adapter/PredisAdapterTest.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php index 5fdd35cafb68c..d9afd85a8e1f6 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php @@ -36,6 +36,8 @@ public function testCreateConnection() $this->assertInstanceOf(StreamConnection::class, $connection); $redisHost = explode(':', $redisHost); + $connectionParameters = $connection->getParameters()->toArray(); + $params = [ 'scheme' => 'tcp', 'host' => $redisHost[0], @@ -46,7 +48,12 @@ public function testCreateConnection() 'tcp_nodelay' => true, 'database' => '1', ]; - $this->assertSame($params, $connection->getParameters()->toArray()); + + if (isset($connectionParameters['conn_uid'])) { + $params['conn_uid'] = $connectionParameters['conn_uid']; // if present, the value cannot be predicted + } + + $this->assertSame($params, $connectionParameters); } public function testCreateSslConnection() @@ -60,6 +67,8 @@ public function testCreateSslConnection() $this->assertInstanceOf(StreamConnection::class, $connection); $redisHost = explode(':', $redisHost); + $connectionParameters = $connection->getParameters()->toArray(); + $params = [ 'scheme' => 'tls', 'host' => $redisHost[0], @@ -71,7 +80,12 @@ public function testCreateSslConnection() 'tcp_nodelay' => true, 'database' => '1', ]; - $this->assertSame($params, $connection->getParameters()->toArray()); + + if (isset($connectionParameters['conn_uid'])) { + $params['conn_uid'] = $connectionParameters['conn_uid']; // if present, the value cannot be predicted + } + + $this->assertSame($params, $connectionParameters); } public function testAclUserPasswordAuth() From c6fcb2fbdc05231c8826352f8ff81acf03456e52 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Feb 2025 12:01:18 +0100 Subject: [PATCH 62/63] Update CHANGELOG for 7.2.4 --- CHANGELOG-7.2.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG-7.2.md b/CHANGELOG-7.2.md index fbc5b88c33e11..1125f1a72875d 100644 --- a/CHANGELOG-7.2.md +++ b/CHANGELOG-7.2.md @@ -7,6 +7,43 @@ 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.4 (2025-02-26) + + * bug #59198 [Messenger] Filter out non-consumable receivers when registering `ConsumeMessagesCommand` (wazum) + * bug #59781 [Mailer] fix multiple transports default injection (fkropfhamer) + * bug #59836 [Mailer][Postmark] Set CID for attachments when it exists (IssamRaouf) + * bug #59829 [FrameworkBundle] Disable the keys normalization of the CSRF form field attributes (sukei) + * bug #59840 Fix PHP warning in GetSetMethodNormalizer when a "set()" method is defined (Pepperoni1337) + * bug #59818 [TypeInfo] Fix create union with nullable type (mtarld) + * bug #59810 [DependencyInjection] Defer check for circular references instead of skipping them (biozshock) + * bug #59811 [Validator] Synchronize IBAN formats (alexandre-daubois) + * bug #59796 [Mime] use address for body at `PathHeader` (tinect) + * bug #59803 [Semaphore] allow redis cluster/sentinel dsn (smoench) + * bug #59779 [DomCrawler] Bug #43921 Check for null parent nodes in the case of orphaned branches (ttk) + * bug #59776 [WebProfilerBundle] fix rendering notifier message options (xabbuh) + * bug #59769 Enable `JSON_PRESERVE_ZERO_FRACTION` in `jsonRequest` method (raffaelecarelle) + * bug #59774 [TwigBridge] Fix compatibility with Twig 3.21 (alexandre-daubois) + * bug #59761 [VarExporter] Fix lazy objects with hooked properties (nicolas-grekas) + * bug #59763 [HttpClient] Don't send any default content-type when the body is empty (nicolas-grekas) + * bug #59747 [Translation] check empty notes (davidvancl) + * bug #59751 [Cache] Tests for Redis Replication with cache (DemigodCode) + * bug #59752 [BrowserKit] Fix submitting forms with empty file fields (nicolas-grekas) + * bug #59742 [Notifier] [BlueSky] Change the value returned as the message ID (javiereguiluz) + * bug #59033 [WebProfilerBundle] Fix interception for non conventional redirects (Huluti) + * bug #59713 [DependencyInjection] Do not preload functions (biozshock) + * bug #59723 [DependencyInjection] Fix cloned lazy services not sharing their dependencies when dumped with PhpDumper (pvandommelen) + * bug #59727 [HttpClient] Fix activity tracking leading to negative timeout errors (nicolas-grekas) + * bug #59728 [Form][FrameworkBundle] Use auto-configuration to make the default CSRF token id apply only to the app; not to bundles (nicolas-grekas) + * bug #59262 [DependencyInjection] Fix env default processor with scalar node (tBibaut) + * bug #59699 [Serializer] Handle default context in named Serializer (HypeMC) + * bug #59640 [Security] Return null instead of empty username to fix deprecation notice (phasdev) + * bug #59661 [Lock] Fix Predis error handling (HypeMC) + * bug #59596 [Mime] use `isRendered` method to ensure we can avoid rendering an email twice (walva) + * bug #59689 [HttpClient] Fix buffering AsyncResponse with no passthru (nicolas-grekas) + * bug #59654 [HttpClient] Fix uploading files > 2GB (nicolas-grekas) + * bug #59648 [HttpClient] Fix retrying requests with `Psr18Client` and NTLM connections (nicolas-grekas, ajgarlag) + * bug #59681 [TypeInfo] Fix promoted property phpdoc reading (mtarld) + * 7.2.3 (2025-01-29) * bug #58889 [Serializer] Handle default context in Serializer (Valmonzo) From c701eb7269d464fafa8d44962cf4baaefa0fbcb4 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Feb 2025 12:01:22 +0100 Subject: [PATCH 63/63] Update VERSION for 7.2.4 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index d6cd83a21161c..ead55c9272043 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.2.4-DEV'; + public const VERSION = '7.2.4'; public const VERSION_ID = 70204; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 2; public const RELEASE_VERSION = 4; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2025'; public const END_OF_LIFE = '07/2025';