diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index d4dafb2aa0029..557eda9c29893 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,22 +1,25 @@
| Q | A
| ------------- | ---
-| Branch? | 7.4 for features / 6.4, 7.2, or 7.3 for bug fixes
+| Branch? | 7.4 for features / 6.4, 7.2, or 7.3 for bug fixes
| Bug fix? | yes/no
-| New feature? | yes/no
-| Deprecations? | yes/no
-| Issues | Fix #...
+| New feature? | yes/no
+| Deprecations? | yes/no
+| Issues | Fix #...
| License | MIT
diff --git a/.github/build-packages.php b/.github/build-packages.php
index d69a3c8198ec0..4793b8483d7ed 100644
--- a/.github/build-packages.php
+++ b/.github/build-packages.php
@@ -1,5 +1,15 @@
'__unset' !== $v);
+ }, []);
+
+ return $expandedVersions ?? [];
+}
+
if (3 > $_SERVER['argc']) {
echo "Usage: branch version dir1 dir2 ... dirN\n";
exit(1);
@@ -52,11 +62,13 @@
$packages[$package->name][$package->version] = $package;
- $versions = @file_get_contents('https://repo.packagist.org/p/'.$package->name.'.json') ?: sprintf('{"packages":{"%s":{"%s":%s}}}', $package->name, $package->version, file_get_contents($dir.'/composer.json'));
- $versions = json_decode($versions)->packages->{$package->name};
+ foreach (['.json', '~dev.json'] as $ext) {
+ $versions = @file_get_contents('https://repo.packagist.org/p2/'.$package->name.$ext) ?: '[]';
+ $versions = json_decode($versions, true)['packages'][$package->name] ?? [];
- foreach ($versions as $v => $package) {
- $packages[$package->name] += [$v => $package];
+ foreach (expandComposerMetadata($versions) as $p) {
+ $packages[$package->name] += [$p['version'] => $p];
+ }
}
}
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index 99d96540eac78..f64569bdc5264 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -67,7 +67,7 @@ jobs:
([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json"
echo COLUMNS=120 >> $GITHUB_ENV
- echo PHPUNIT="$(pwd)/phpunit --exclude-group tty,benchmark,intl-data,integration" >> $GITHUB_ENV
+ echo PHPUNIT="$(pwd)/phpunit --exclude-group tty,benchmark,intl-data,integration,transient" >> $GITHUB_ENV
echo COMPOSER_UP='composer update --no-progress --ansi'$([[ "${{ matrix.mode }}" != low-deps ]] && echo ' --ignore-platform-req=php+') >> $GITHUB_ENV
SYMFONY_VERSIONS=$(git ls-remote -q --heads | cut -f2 | grep -o '/[1-9][0-9]*\.[0-9].*' | sort -V)
@@ -92,7 +92,7 @@ jobs:
# Create local composer packages for each patched components and reference them in composer.json files when cross-testing components
if [[ ! "${{ matrix.mode }}" = *-deps ]]; then
- php .github/build-packages.php HEAD^ $SYMFONY_VERSION src/Symfony/Bridge/PhpUnit
+ php .github/build-packages.php HEAD^ $SYMFONY_VERSION src/Symfony/Bridge/PhpUnit
else
echo SYMFONY_DEPRECATIONS_HELPER=weak >> $GITHUB_ENV
cp composer.json composer.json.orig
@@ -212,7 +212,7 @@ jobs:
export SYMFONY_REQUIRE=">=$SYMFONY_VERSION"
git fetch --depth=2 origin $SYMFONY_VERSION
git checkout -m FETCH_HEAD
- PATCHED_COMPONENTS=$(echo "$PATCHED_COMPONENTS" | xargs dirname | xargs -n1 -I{} bash -c "[ -e '{}/phpunit.xml.dist' ] && echo '{}'" | sort || true)
+ PATCHED_COMPONENTS=$(echo "$PATCHED_COMPONENTS" | xargs dirname | xargs -I{} bash -c "[ -e '{}/phpunit.xml.dist' ] && echo '{}'" | sort || true)
if [[ $PATCHED_COMPONENTS ]]; then
echo "::group::install phpunit"
./phpunit install
diff --git a/CHANGELOG-7.2.md b/CHANGELOG-7.2.md
index d6d188669de42..d128815948827 100644
--- a/CHANGELOG-7.2.md
+++ b/CHANGELOG-7.2.md
@@ -7,6 +7,55 @@ 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.8 (2025-06-28)
+
+ * bug #60044 [Console] Table counts wrong column width when using colspan and `setColumnMaxWidth()` (vladimir-vv)
+ * bug #60042 [Console] Table counts wrong number of padding symbols in `renderCell()` method when cell contain unicode variant selector (vladimir-vv)
+ * bug #60594 [Cache] Fix using a `ChainAdapter` as an adapter for a pool (IndraGunawan)
+ * bug #60483 [HttpKernel] Fix `#[MapUploadedFile]` handling for optional file uploads (santysisi)
+ * bug #60413 [Serializer] Fix collect_denormalization_errors flag in defaultContext (dmbrson)
+ * bug #60820 [TypeInfo] Fix handling `ConstFetchNode` (norkunas)
+ * bug #60908 [Uid] Improve entropy of the increment for UUIDv7 (nicolas-grekas)
+ * bug #60914 [Console] Fix command option mode (InputOption::VALUE_REQUIRED) (gharlan)
+ * bug #60919 [VarDumper] Avoid deprecated call in PgSqlCaster (vrana)
+ * bug #60909 [TypeInfo] use an EOL-agnostic approach to parse class uses (xabbuh)
+ * bug #60888 [Intl] Fix locale validator when canonicalize is true (rdavaillaud)
+ * bug #60885 [Notifier] Update fake SMS transports to use contracts event dispatcher (paulferrett)
+ * bug #60859 [TwigBundle] fix preload unlinked class `BinaryOperatorExpressionParser` (Grummfy)
+ * bug #60772 [Mailer] [Transport] Send clone of `RawMessage` instance in `RoundRobinTransport` (jnoordsij)
+ * bug #60842 [DependencyInjection] Fix generating adapters of functional interfaces (nicolas-grekas)
+ * bug #60809 [Serializer] Fix `TraceableSerializer` when called from a callable inside `array_map` (OrestisZag)
+ * bug #60511 [Serializer] Add support for discriminator map in property normalizer (ruudk)
+ * bug #60780 [FrameworkBundle] Fix argument not provided to `add_bus_name_stamp_middleware` (maxbaldanza)
+ * bug #60826 [DependencyInjection] Fix inlining when public services are involved (nicolas-grekas)
+ * bug #60806 [HttpClient] Limit curl's connection cache size (nicolas-grekas)
+ * bug #60705 [FrameworkBundle] Fix allow `loose` as an email validation mode (rhel-eo)
+ * bug #60759 [Messenger] Fix float value for worker memory limit (ro0NL)
+ * bug #60785 [Security] Handle non-callable implementations of `FirewallListenerInterface` (MatTheCat)
+ * bug #60781 [DomCrawler] Allow selecting `button`s by their `value` (MatTheCat)
+ * bug #60775 [Validator] flip excluded properties with keys with Doctrine-style constraint config (xabbuh)
+ * bug #60774 [FrameworkBundle] Fixes getting a type error when the secret you are trying to reveal could not be decrypted (jack-worman)
+ * bug #60779 Silence E_DEPRECATED and E_USER_DEPRECATED (nicolas-grekas)
+ * bug #60502 [HttpCache] Hit the backend only once after waiting for the cache lock (mpdude)
+ * bug #60771 [Runtime] fix compatibility with Symfony 7.4 (xabbuh)
+ * bug #59910 [Form] Keep submitted values when `keep_as_list` option of collection type is enabled (kells)
+ * bug #60638 [Form] Fix `keep_as_list` when data is not an array (MatTheCat)
+ * bug #60691 [DependencyInjection] Fix `ServiceLocatorTagPass` indexes handling (MatTheCat)
+ * bug #60676 [Form] Fix handling the empty string in NumberToLocalizedStringTransformer (gnat42)
+ * bug #60694 [Intl] Add missing currency (NOK) localization (en_NO) (llupa)
+ * bug #60711 [Intl] Ensure data consistency between alpha and numeric codes (llupa)
+ * bug #60724 [VarDumper] Fix dumping LazyObjectState when using VarExporter v8 (nicolas-grekas)
+ * bug #60693 [FrameworkBundle] ensureKernelShutdown in tearDownAfterClass (cquintana92)
+ * bug #60564 [FrameworkBundle] ensureKernelShutdown in tearDownAfterClass (cquintana92)
+ * bug #60645 [PhpUnitBridge] Skip bootstrap for PHPUnit >=10 (HypeMC)
+ * bug #60655 [TypeInfo] Handle `key-of` and `value-of` types (mtarld)
+ * bug #60640 [Mailer] use STARTTLS for SMTP with MailerSend (xabbuh)
+ * bug #60648 [Yaml] fix support for years outside of the 32b range on x86 arch on PHP 8.4 (nicolas-grekas)
+ * bug #60616 skip interactive questions asked by Composer (xabbuh)
+ * bug #60584 [DependencyInjection] Make `YamlDumper` quote resolved env vars if necessary (MatTheCat)
+ * bug #60588 [Notifier][Clicksend] Fix lack of recipient in case DSN does not have optional LIST_ID param (alifanau)
+ * bug #60547 [HttpFoundation] Fixed 'Via' header regex (thecaliskan)
+
* 7.2.7 (2025-05-29)
* bug #60549 [Translation] Add intl-icu fallback for MessageCatalogue metadata (pontus-mp)
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ee2cb2a40889b..3e7f5ec2b6e78 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -30,9 +30,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Kris Wallsmith (kriswallsmith)
- Jakub Zalas (jakubzalas)
- Yonel Ceruto (yonelceruto)
+ - HypeMC (hypemc)
- Hugo Hamon (hhamon)
- Tobias Nyholm (tobias)
- - HypeMC (hypemc)
- Jérôme Tamarelle (gromnan)
- Antoine Lamirault (alamirault)
- Samuel ROZE (sroze)
@@ -96,8 +96,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Henrik Bjørnskov (henrikbjorn)
- Ruud Kamphuis (ruudk)
- David Buchmann (dbu)
- - Andrej Hudec (pulzarraider)
- Tomas Norkūnas (norkunas)
+ - Andrej Hudec (pulzarraider)
- Jáchym Toušek (enumag)
- Hubert Lenoir (hubert_lenoir)
- Christian Raue
@@ -160,12 +160,13 @@ The Symfony Connect username in parenthesis allows to get more information
- Włodzimierz Gajda (gajdaw)
- Javier Spagnoletti (phansys)
- Adrien Brault (adrienbrault)
+ - Florent Morselli (spomky_)
+ - soyuka
- Florian Voutzinos (florianv)
- Teoh Han Hui (teohhanhui)
- Przemysław Bogusz (przemyslaw-bogusz)
- Colin Frei
- excelwebzone
- - Florent Morselli (spomky_)
- Paráda József (paradajozsef)
- Maximilian Beckers (maxbeckers)
- Baptiste Clavié (talus)
@@ -175,17 +176,16 @@ The Symfony Connect username in parenthesis allows to get more information
- Dāvis Zālītis (k0d3r1s)
- Gordon Franke (gimler)
- Malte Schlüter (maltemaltesich)
- - soyuka
- jeremyFreeAgent (jeremyfreeagent)
- Michael Babker (mbabker)
- Alexis Lefebvre
+ - Hugo Alliaume (kocal)
- Christopher Hertel (chertel)
- Joshua Thijssen
- Vasilij Dusko
- Daniel Wehner (dawehner)
- Robert Schönthal (digitalkaoz)
- Smaine Milianni (ismail1432)
- - Hugo Alliaume (kocal)
- François-Xavier de Guillebon (de-gui_f)
- Andreas Schempp (aschempp)
- noniagriconomie
@@ -255,6 +255,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Alessandro Lai (jean85)
- 77web
- Gocha Ossinkine (ossinkine)
+ - matlec
- Jesse Rushlow (geeshoe)
- Matthieu Ouellette-Vachon (maoueh)
- Michał Pipa (michal.pipa)
@@ -286,7 +287,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Clément JOBEILI (dator)
- Andreas Möller (localheinz)
- Marek Štípek (maryo)
- - matlec
- Daniel Espendiller
- Arnaud PETITPAS (apetitpa)
- Michael Käfer (michael_kaefer)
@@ -310,6 +310,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Patrick Landolt (scube)
- Karoly Gossler (connorhu)
- Timo Bakx (timobakx)
+ - Quentin Devos
- Giorgio Premi
- Alan Poulain (alanpoulain)
- Ruben Gonzalez (rubenrua)
@@ -337,6 +338,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Nikolay Labinskiy (e-moe)
- Martin Schuhfuß (usefulthink)
- apetitpa
+ - wkania
- Guilliam Xavier
- Pierre Minnieur (pminnieur)
- Dominique Bongiraud
@@ -377,6 +379,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Pascal Montoya
- Julien Brochet
- François Pluchino (francoispluchino)
+ - W0rma
- Tristan Darricau (tristandsensio)
- Jan Sorgalla (jsor)
- henrikbjorn
@@ -401,7 +404,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Zan Baldwin (zanbaldwin)
- Tim Goudriaan (codedmonkey)
- BoShurik
- - Quentin Devos
- Adam Prager (padam87)
- Benoît Burnichon (bburnichon)
- maxime.steinhausser
@@ -428,7 +430,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Uwe Jäger (uwej711)
- javaDeveloperKid
- Chris Smith (cs278)
- - W0rma
- Lynn van der Berg (kjarli)
- Michaël Perrin (michael.perrin)
- Eugene Leonovich (rybakit)
@@ -438,6 +439,7 @@ The Symfony Connect username in parenthesis allows to get more information
- GordonsLondon
- Ray
- Philipp Cordes (corphi)
+ - Fabien S (bafs)
- Chekote
- Thomas Adam
- Anderson Müller
@@ -471,6 +473,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Marcos Sánchez
- Emanuele Panzeri (thepanz)
- Zmey
+ - Santiago San Martin (santysisi)
- Kim Hemsø Rasmussen (kimhemsoe)
- Maximilian Reichel (phramz)
- Samaël Villette (samadu61)
@@ -498,6 +501,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Manuel Kießling (manuelkiessling)
- Alexey Kopytko (sanmai)
- Warxcell (warxcell)
+ - SiD (plbsid)
- Atsuhiro KUBO (iteman)
- rudy onfroy (ronfroy)
- Serkan Yildiz (srknyldz)
@@ -507,7 +511,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Gabor Toth (tgabi333)
- realmfoo
- Joppe De Cuyper (joppedc)
- - Fabien S (bafs)
- Simon Podlipsky (simpod)
- Thomas Tourlourat (armetiz)
- Andrey Esaulov (andremaha)
@@ -612,7 +615,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Alex (aik099)
- Kieran Brahney
- Fabien Villepinte
- - SiD (plbsid)
- Greg Thornton (xdissent)
- Alex Bowers
- Kev
@@ -638,6 +640,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Ivan Sarastov (isarastov)
- flack (flack)
- Shein Alexey
+ - Link1515
- Joe Lencioni
- Daniel Tschinder
- Diego Agulló (aeoris)
@@ -758,6 +761,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jérémy REYNAUD (babeuloula)
- Faizan Akram Dar (faizanakram)
- Arkadius Stefanski (arkadius)
+ - Andy Palmer (andyexeter)
- Jonas Flodén (flojon)
- AnneKir
- Tobias Weichart
@@ -781,6 +785,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Giso Stallenberg (gisostallenberg)
- Rob Bast
- Roberto Espinoza (respinoza)
+ - Steven RENAUX (steven_renaux)
- Marvin Feldmann (breyndotechse)
- Soufian EZ ZANTAR (soezz)
- Marek Zajac
@@ -867,7 +872,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Dariusz Ruminski
- Bahman Mehrdad (bahman)
- Romain Gautier (mykiwi)
- - Link1515
- Matthieu Bontemps
- Erik Trapman
- De Cock Xavier (xdecock)
@@ -1010,7 +1014,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Jonas Elfering
- Mihai Stancu
- Nahuel Cuesta (ncuesta)
- - Santiago San Martin
- Chris Boden (cboden)
- EStyles (insidestyles)
- Christophe Villeger (seragan)
@@ -1065,7 +1068,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Pierrick VIGNAND (pierrick)
- Alex Bogomazov (alebo)
- aaa2000 (aaa2000)
- - Andy Palmer (andyexeter)
- Andrew Neil Forster (krciga22)
- Stefan Warman (warmans)
- Tristan Maindron (tmaindron)
@@ -1865,6 +1867,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Philipp Fritsche
- Léon Gersen
- tarlepp
+ - Giuseppe Arcuti
- Dustin Wilson
- Benjamin Paap (benjaminpaap)
- Claus Due (namelesscoder)
@@ -1958,7 +1961,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Bruno MATEU
- Jeremy Bush
- Lucas Bäuerle
- - Steven RENAUX (steven_renaux)
- Laurens Laman
- Thomason, James
- Dario Savella
@@ -2195,6 +2197,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Tim Ward
- Adiel Cristo (arcristo)
- Christian Flach (cmfcmf)
+ - Dennis Jaschinski (d.jaschinski)
- Fabian Kropfhamer (fabiank)
- Jeffrey Cafferata (jcidnl)
- Junaid Farooq (junaidfarooq)
@@ -2264,6 +2267,7 @@ The Symfony Connect username in parenthesis allows to get more information
- wivaku
- Markus Reinhold
- Jingyu Wang
+ - es
- steveYeah
- Asrorbek (asrorbek)
- Samy D (dinduks)
@@ -2278,6 +2282,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Alan Scott
- Juanmi Rodriguez Cerón
- twifty
+ - David Szkiba
- Andy Raines
- François Poguet
- Anthony Ferrara
@@ -2296,6 +2301,7 @@ The Symfony Connect username in parenthesis allows to get more information
- xdavidwu
- Benjamin RICHARD
- Raphaël Droz
+ - Vladimir Pakhomchik
- pdommelen
- Eric Stern
- ShiraNai7
@@ -2710,6 +2716,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Marcel Siegert
- ryunosuke
- Bruno BOUTAREL
+ - Athorcis
- John Stevenson
- everyx
- Richard Heine
@@ -2767,6 +2774,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Abdouarrahmane FOUAD (fabdouarrahmane)
- Jakub Janata (janatjak)
- Jm Aribau (jmaribau)
+ - Maciej Paprocki (maciekpaprocki)
- Matthew Foster (mfoster)
- Paul Seiffert (seiffert)
- Vasily Khayrulin (sirian)
@@ -3114,6 +3122,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Darryl Hein (xmmedia)
- Vladimir Sadicov (xtech)
- Marcel Berteler
+ - Ruud Seberechts
- sdkawata
- Frederik Schmitt
- Peter van Dommelen
@@ -3151,6 +3160,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Pierre Rineau
- Florian Morello
- Maxim Lovchikov
+ - ivelin vasilev
- adenkejawen
- Florent SEVESTRE (aniki-taicho)
- Ari Pringle (apringle)
@@ -3327,6 +3337,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Kevin Verschaeve (keversc)
- Kevin Herrera (kherge)
- Kubicki Kamil (kubik)
+ - Lauris Binde (laurisb)
- Luis Ramón López López (lrlopez)
- Vladislav Nikolayev (luxemate)
- Martin Mandl (m2mtech)
@@ -3372,7 +3383,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Youpie
- Jason Stephens
- Korvin Szanto
- - wkania
- srsbiz
- Taylan Kasap
- Michael Orlitzky
@@ -3585,6 +3595,7 @@ The Symfony Connect username in parenthesis allows to get more information
- mieszko4
- Steve Preston
- ibasaw
+ - koyolgecen
- Wojciech Skorodecki
- Kevin Frantz
- Neophy7e
@@ -3614,6 +3625,7 @@ The Symfony Connect username in parenthesis allows to get more information
- satalaondrej
- Matthias Dötsch
- jonmldr
+ - Nowfel2501
- Yevgen Kovalienia
- Lebnik
- Shude
@@ -3635,6 +3647,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Egor Gorbachev
- Julian Krzefski
- Derek Stephen McLean
+ - PatrickRedStar
- Norman Soetbeer
- zorn
- Yuriy Potemkin
@@ -3744,6 +3757,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Brandon Kelly (brandonkelly)
- Choong Wei Tjeng (choonge)
- Bermon Clément (chou666)
+ - Chris Shennan (chrisshennan)
- Citia (citia)
- Kousuke Ebihara (co3k)
- Loïc Vernet (coil)
@@ -3906,6 +3920,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Romain
- Xavier REN
- Kevin Meijer
+ - Ignacio Alveal
- max
- Alexander Bauer (abauer)
- Ahmad Mayahi (ahmadmayahi)
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 6909669ee14a8..27418b4002971 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -54,7 +54,7 @@
./src/Symfony/Bridge/*/Tests./src/Symfony/Component/*/Tests./src/Symfony/Component/*/*/Tests
- ./src/Symfony/Contract/*/Tests
+ ./src/Symfony/Contracts/*/Tests./src/Symfony/Bundle/*/Tests./src/Symfony/Bundle/*/Resources./src/Symfony/Component/*/Resources
diff --git a/src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php b/src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php
index 40472ff73ef40..d96416b287c65 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php
@@ -58,9 +58,11 @@ public static function createTestConfiguration(): Configuration
{
$config = ORMSetup::createConfiguration(true);
$config->setEntityNamespaces(['SymfonyTestsDoctrine' => 'Symfony\Bridge\Doctrine\Tests\Fixtures']);
- $config->setAutoGenerateProxyClasses(true);
- $config->setProxyDir(sys_get_temp_dir());
- $config->setProxyNamespace('SymfonyTests\Doctrine');
+ if (\PHP_VERSION_ID < 80400 || !method_exists($config, 'enableNativeLazyObjects')) {
+ $config->setAutoGenerateProxyClasses(true);
+ $config->setProxyDir(sys_get_temp_dir());
+ $config->setProxyNamespace('SymfonyTests\Doctrine');
+ }
$config->setMetadataDriverImpl(new AttributeDriver([__DIR__.'/../Tests/Fixtures' => 'Symfony\Bridge\Doctrine\Tests\Fixtures'], true));
$config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
$config->setLazyGhostObjectEnabled(true);
diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php
index 7903da227e912..7b8bbf579b6bc 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php
@@ -43,7 +43,11 @@ private function createExtractor(): DoctrineExtractor
$config = ORMSetup::createConfiguration(true);
$config->setMetadataDriverImpl(new AttributeDriver([__DIR__.'/../Tests/Fixtures' => 'Symfony\Bridge\Doctrine\Tests\Fixtures'], true));
$config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
- $config->setLazyGhostObjectEnabled(true);
+ if (\PHP_VERSION_ID >= 80400 && method_exists($config, 'enableNativeLazyObjects')) {
+ $config->enableNativeLazyObjects(true);
+ } else {
+ $config->setLazyGhostObjectEnabled(true);
+ }
$eventManager = new EventManager();
$entityManager = new EntityManager(DriverManager::getConnection(['driver' => 'pdo_sqlite'], $config, $eventManager), $config, $eventManager);
diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php
index 590a886350c79..78a6ff75d2672 100644
--- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php
+++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php
@@ -138,6 +138,7 @@
'COMPOSER' => 'composer.json',
'COMPOSER_VENDOR_DIR' => 'vendor',
'COMPOSER_BIN_DIR' => 'bin',
+ 'COMPOSER_NO_INTERACTION' => '1',
'SYMFONY_SIMPLE_PHPUNIT_BIN_DIR' => __DIR__,
];
@@ -234,10 +235,10 @@
@copy("$PHPUNIT_VERSION_DIR/phpunit.xsd", 'phpunit.xsd');
chdir("$PHPUNIT_VERSION_DIR");
if ($SYMFONY_PHPUNIT_REMOVE) {
- $passthruOrFail("$COMPOSER remove --no-update --no-interaction ".$SYMFONY_PHPUNIT_REMOVE);
+ $passthruOrFail("$COMPOSER remove --no-update ".$SYMFONY_PHPUNIT_REMOVE);
}
if ($SYMFONY_PHPUNIT_REQUIRE) {
- $passthruOrFail("$COMPOSER require --no-update --no-interaction ".$SYMFONY_PHPUNIT_REQUIRE);
+ $passthruOrFail("$COMPOSER require --no-update ".$SYMFONY_PHPUNIT_REQUIRE);
}
if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) {
$passthruOrFail("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\"");
diff --git a/src/Symfony/Bridge/PhpUnit/bootstrap.php b/src/Symfony/Bridge/PhpUnit/bootstrap.php
index f11b7ab7f4945..50157aec4ad0f 100644
--- a/src/Symfony/Bridge/PhpUnit/bootstrap.php
+++ b/src/Symfony/Bridge/PhpUnit/bootstrap.php
@@ -14,7 +14,11 @@
use Symfony\Bridge\PhpUnit\DeprecationErrorHandler;
// Detect if we need to serialize deprecations to a file.
-if (in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $file = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')) {
+if (
+ // Skip if we're using PHPUnit >=10
+ !class_exists(PHPUnit\Metadata\Metadata::class)
+ && in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $file = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')
+) {
DeprecationErrorHandler::collectDeprecations($file);
return;
@@ -46,6 +50,10 @@
}
}
-if ('disabled' !== getenv('SYMFONY_DEPRECATIONS_HELPER')) {
+if (
+ // Skip if we're using PHPUnit >=10
+ !class_exists(PHPUnit\Metadata\Metadata::class, false)
+ && 'disabled' !== getenv('SYMFONY_DEPRECATIONS_HELPER')
+) {
DeprecationErrorHandler::register(getenv('SYMFONY_DEPRECATIONS_HELPER'));
}
diff --git a/src/Symfony/Bridge/PhpUnit/composer.json b/src/Symfony/Bridge/PhpUnit/composer.json
index 1283dfe33a9b0..de9101f796d73 100644
--- a/src/Symfony/Bridge/PhpUnit/composer.json
+++ b/src/Symfony/Bridge/PhpUnit/composer.json
@@ -2,7 +2,9 @@
"name": "symfony/phpunit-bridge",
"type": "symfony-bridge",
"description": "Provides utilities for PHPUnit, especially user deprecation notices management",
- "keywords": [],
+ "keywords": [
+ "testing"
+ ],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php
index 150186b1d37ba..c2110ee76f683 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php
@@ -61,6 +61,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if (!\array_key_exists($name, $secrets)) {
$io->error(\sprintf('The secret "%s" does not exist.', $name));
+ return self::INVALID;
+ } elseif (null === $secrets[$name]) {
+ $io->error(\sprintf('The secret "%s" could not be decrypted.', $name));
+
return self::INVALID;
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php
index 9cdfdae04cb37..a320130d5a6e7 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php
@@ -69,7 +69,7 @@ protected function configure(): void
->setDefinition([
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'),
new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'),
- new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'The messages domain'),
+ new InputOption('domain', null, InputOption::VALUE_REQUIRED, 'The messages domain'),
new InputOption('only-missing', null, InputOption::VALUE_NONE, 'Display only missing messages'),
new InputOption('only-unused', null, InputOption::VALUE_NONE, 'Display only unused messages'),
new InputOption('all', null, InputOption::VALUE_NONE, 'Load messages from all registered bundles'),
diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
index 194d1c50d25d5..25fd1c91e2226 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
@@ -74,15 +74,15 @@ protected function configure(): void
->setDefinition([
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'),
new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'),
- new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'),
+ new InputOption('prefix', null, InputOption::VALUE_REQUIRED, 'Override the default prefix', '__'),
new InputOption('no-fill', null, InputOption::VALUE_NONE, 'Extract translation keys without filling in values'),
- new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf12'),
+ new InputOption('format', null, InputOption::VALUE_REQUIRED, 'Override the default output format', 'xlf12'),
new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Should the extract be done'),
new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'),
- new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to extract'),
- new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically'),
- new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'),
+ new InputOption('domain', null, InputOption::VALUE_REQUIRED, 'Specify the domain to extract'),
+ new InputOption('sort', null, InputOption::VALUE_REQUIRED, 'Return list of messages sorted alphabetically'),
+ new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'),
])
->setHelp(<<<'EOF'
The %command.name% command extracts translation strings from templates
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 5154393f2769e..f6b40030bccce 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -1020,7 +1020,7 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e
->validate()->castToArray()->end()
->end()
->scalarNode('translation_domain')->defaultValue('validators')->end()
- ->enumNode('email_validation_mode')->values((class_exists(Email::class) ? Email::VALIDATION_MODES : ['html5-allow-no-tld', 'html5', 'strict']) + ['loose'])->defaultValue('html5')->end()
+ ->enumNode('email_validation_mode')->values(array_merge(class_exists(Email::class) ? Email::VALIDATION_MODES : ['html5-allow-no-tld', 'html5', 'strict'], ['loose']))->defaultValue('html5')->end()
->arrayNode('mapping')
->addDefaultsIfNotSet()
->fixXmlConfig('path')
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 3c4e4f9f3a5eb..c9d138fa5a3a5 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -1630,10 +1630,6 @@ private function registerValidationConfiguration(array $config, ContainerBuilder
throw new LogicException('Validation support cannot be enabled as the Validator component is not installed. Try running "composer require symfony/validator".');
}
- if (!isset($config['email_validation_mode'])) {
- $config['email_validation_mode'] = 'loose';
- }
-
$loader->load('validator.php');
$validatorBuilder = $container->getDefinition('validator.builder');
@@ -2180,16 +2176,18 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
$defaultMiddleware['after'][0]['arguments'] = [$bus['default_middleware']['allow_no_senders']];
$defaultMiddleware['after'][1]['arguments'] = [$bus['default_middleware']['allow_no_handlers']];
- // argument to add_bus_name_stamp_middleware
- $defaultMiddleware['before'][0]['arguments'] = [$busId];
-
$middleware = array_merge($defaultMiddleware['before'], $middleware, $defaultMiddleware['after']);
}
- foreach ($middleware as $middlewareItem) {
+ foreach ($middleware as $key => $middlewareItem) {
if (!$validationEnabled && \in_array($middlewareItem['id'], ['validation', 'messenger.middleware.validation'], true)) {
throw new LogicException('The Validation middleware is only available when the Validator component is installed and enabled. Try running "composer require symfony/validator".');
}
+
+ // argument to add_bus_name_stamp_middleware
+ if ('add_bus_name_stamp_middleware' === $middlewareItem['id']) {
+ $middleware[$key]['arguments'] = [$busId];
+ }
}
if ($container->getParameter('kernel.debug') && class_exists(Stopwatch::class)) {
diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.php
index 882ec78628839..788601d2e91ed 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.php
@@ -31,6 +31,9 @@ abstract public function reveal(string $name): ?string;
abstract public function remove(string $name): bool;
+ /**
+ * @return array
+ */
abstract public function list(bool $reveal = false): array;
protected function validateName(string $name): void
diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php
index 15952611ac1a1..3fab5f4e28525 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php
@@ -89,13 +89,13 @@ public function list(bool $reveal = false): array
foreach ($_ENV as $k => $v) {
if ('' !== ($v ?? '') && preg_match('/^\w+$/D', $k)) {
- $secrets[$k] = $reveal ? $v : null;
+ $secrets[$k] = \is_string($v) && $reveal ? $v : null;
}
}
foreach ($_SERVER as $k => $v) {
if ('' !== ($v ?? '') && preg_match('/^\w+$/D', $k)) {
- $secrets[$k] = $reveal ? $v : null;
+ $secrets[$k] = \is_string($v) && $reveal ? $v : null;
}
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
index b2c2eb4d23089..87925f73c9b52 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
@@ -39,6 +39,14 @@ protected function tearDown(): void
static::$booted = false;
}
+ public static function tearDownAfterClass(): void
+ {
+ static::ensureKernelShutdown();
+ static::$class = null;
+ static::$kernel = null;
+ static::$booted = false;
+ }
+
/**
* @throws \RuntimeException
* @throws \LogicException
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php
index 94643db2c92c5..d77d303d5c88b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php
@@ -46,6 +46,19 @@ public function testInvalidName()
$this->assertStringContainsString('The secret "undefinedKey" does not exist.', trim($tester->getDisplay(true)));
}
+ public function testFailedDecrypt()
+ {
+ $vault = $this->createMock(AbstractVault::class);
+ $vault->method('list')->willReturn(['secretKey' => null]);
+
+ $command = new SecretsRevealCommand($vault);
+
+ $tester = new CommandTester($command);
+ $this->assertSame(Command::INVALID, $tester->execute(['name' => 'secretKey']));
+
+ $this->assertStringContainsString('The secret "secretKey" could not be decrypted.', trim($tester->getDisplay(true)));
+ }
+
/**
* @backupGlobals enabled
*/
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_bus_name_stamp.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_bus_name_stamp.php
new file mode 100644
index 0000000000000..5c9c3c31018aa
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_bus_name_stamp.php
@@ -0,0 +1,24 @@
+loadFromExtension('framework', [
+ 'annotations' => false,
+ 'http_method_override' => false,
+ 'handle_all_throwables' => true,
+ 'php_errors' => ['log' => true],
+ 'messenger' => [
+ 'default_bus' => 'messenger.bus.commands',
+ 'buses' => [
+ 'messenger.bus.commands' => [
+ 'default_middleware' => false,
+ 'middleware' => [
+ 'add_bus_name_stamp_middleware',
+ 'send_message',
+ 'handle_message',
+ ],
+ ],
+ 'messenger.bus.events' => [
+ 'default_middleware' => true,
+ ],
+ ],
+ ],
+]);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_bus_name_stamp.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_bus_name_stamp.xml
new file mode 100644
index 0000000000000..bef24ed53c7a3
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_bus_name_stamp.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_bus_name_stamp.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_bus_name_stamp.yml
new file mode 100644
index 0000000000000..954c66ae95f6f
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_bus_name_stamp.yml
@@ -0,0 +1,17 @@
+framework:
+ annotations: false
+ http_method_override: false
+ handle_all_throwables: true
+ php_errors:
+ log: true
+ messenger:
+ default_bus: messenger.bus.commands
+ buses:
+ messenger.bus.commands:
+ default_middleware: false
+ middleware:
+ - "add_bus_name_stamp_middleware"
+ - "send_message"
+ - "handle_message"
+ messenger.bus.events:
+ default_middleware: true
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
index 655f9180eceb2..c933f0edf890b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
@@ -1093,6 +1093,28 @@ public function testMessengerWithMultipleBuses()
$this->assertSame('messenger.bus.commands', (string) $container->getAlias('messenger.default_bus'));
}
+ public function testMessengerWithAddBusNameStampMiddleware()
+ {
+ $container = $this->createContainerFromFile('messenger_bus_name_stamp');
+
+ $this->assertTrue($container->has('messenger.bus.commands'));
+ $this->assertEquals([
+ ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']],
+ ['id' => 'send_message', 'arguments' => []],
+ ['id' => 'handle_message', 'arguments' => []],
+ ], $container->getParameter('messenger.bus.commands.middleware'));
+ $this->assertTrue($container->has('messenger.bus.events'));
+ $this->assertSame([], $container->getDefinition('messenger.bus.events')->getArgument(0));
+ $this->assertEquals([
+ ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']],
+ ['id' => 'reject_redelivered_message_middleware'],
+ ['id' => 'dispatch_after_current_bus'],
+ ['id' => 'failed_message_processing_middleware'],
+ ['id' => 'send_message', 'arguments' => [true]],
+ ['id' => 'handle_message', 'arguments' => [false]],
+ ], $container->getParameter('messenger.bus.events.middleware'));
+ }
+
public function testMessengerMiddlewareFactoryErroneousFormat()
{
$this->expectException(\InvalidArgumentException::class);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
index ad979a01a96b3..e30db4852c9e2 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
@@ -292,5 +292,6 @@ public static function emailValidationModeProvider()
foreach (Email::VALIDATION_MODES as $mode) {
yield [$mode];
}
+ yield ['loose'];
}
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php
index 5067a880ddbf8..95dcc36edcc4e 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php
@@ -53,7 +53,7 @@ public function testNoDebug()
public function testNoDumpedXML()
{
- static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true, 'debug.container.dump' => false]);
+ static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'no_dump.yml', 'debug' => true]);
$application = new Application(static::$kernel);
$application->setAutoExit(false);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/no_dump.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/no_dump.yml
new file mode 100644
index 0000000000000..a9c709e9a6425
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/no_dump.yml
@@ -0,0 +1,5 @@
+imports:
+ - { resource: config.yml }
+
+parameters:
+ debug.container.dump: false
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
index 14e7e45a1dc5c..a07c6b346cfe7 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
@@ -319,7 +319,7 @@ private function createFirewalls(array $config, ContainerBuilder $container): vo
$authenticators[$name] = ServiceLocatorTagPass::register($container, $firewallAuthenticatorRefs);
}
$contextId = 'security.firewall.map.context.'.$name;
- $isLazy = !$firewall['stateless'] && (!empty($firewall['anonymous']['lazy']) || $firewall['lazy']);
+ $isLazy = !$firewall['stateless'] && $firewall['lazy'];
$context = new ChildDefinition($isLazy ? 'security.firewall.lazy_context' : 'security.firewall.context');
$context = $container->setDefinition($contextId, $context);
$context
@@ -681,7 +681,7 @@ private function getUserProvider(ContainerBuilder $container, string $id, array
return $this->createMissingUserProvider($container, $id, $factoryKey);
}
- if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) {
+ if ('remember_me' === $factoryKey) {
return 'security.user_providers';
}
diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php
index 63648bd67510e..7263f4247959b 100644
--- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php
+++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php
@@ -12,6 +12,7 @@
namespace Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
@@ -23,7 +24,7 @@
class FirewallContext
{
/**
- * @param iterable $listeners
+ * @param iterable $listeners
*/
public function __construct(
private iterable $listeners,
@@ -39,7 +40,7 @@ public function getConfig(): ?FirewallConfig
}
/**
- * @return iterable
+ * @return iterable
*/
public function getListeners(): iterable
{
diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
index 02631d28c39a4..bc721a2798fcc 100644
--- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
+++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
@@ -44,6 +44,7 @@
use Twig\Extension\OptimizerExtension;
use Twig\Extension\StagingExtension;
use Twig\ExtensionSet;
+use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;
use Twig\Profiler\Profile;
@@ -63,6 +64,7 @@
->tag('container.preload', ['class' => EscaperExtension::class])
->tag('container.preload', ['class' => OptimizerExtension::class])
->tag('container.preload', ['class' => StagingExtension::class])
+ ->tag('container.preload', ['class' => BinaryOperatorExpressionParser::class])
->tag('container.preload', ['class' => ExtensionSet::class])
->tag('container.preload', ['class' => Template::class])
->tag('container.preload', ['class' => TemplateWrapper::class])
diff --git a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php
index b4d77fd74a7a6..1a8576477c801 100644
--- a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php
+++ b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php
@@ -55,9 +55,11 @@ public function process(ContainerBuilder $container): void
continue;
}
$class = $adapter->getClass();
+ $providers = $adapter->getArguments();
while ($adapter instanceof ChildDefinition) {
$adapter = $container->findDefinition($adapter->getParent());
$class = $class ?: $adapter->getClass();
+ $providers += $adapter->getArguments();
if ($t = $adapter->getTag('cache.pool')) {
$tags[0] += $t[0];
}
@@ -87,7 +89,7 @@ public function process(ContainerBuilder $container): void
if (ChainAdapter::class === $class) {
$adapters = [];
- foreach ($adapter->getArgument(0) as $provider => $adapter) {
+ foreach ($providers['index_0'] ?? $providers[0] as $provider => $adapter) {
if ($adapter instanceof ChildDefinition) {
$chainedPool = $adapter;
} else {
diff --git a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php
index ef64d1932da8f..6527cceff47f7 100644
--- a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php
+++ b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php
@@ -209,7 +209,8 @@ public function testChainAdapterPool()
$container->register('cache.adapter.apcu', ApcuAdapter::class)
->setArguments([null, 0, null])
->addTag('cache.pool');
- $container->register('cache.chain', ChainAdapter::class)
+ $container->register('cache.adapter.chain', ChainAdapter::class);
+ $container->setDefinition('cache.chain', new ChildDefinition('cache.adapter.chain'))
->addArgument(['cache.adapter.array', 'cache.adapter.apcu'])
->addTag('cache.pool');
$container->setDefinition('cache.app', new ChildDefinition('cache.chain'))
@@ -224,7 +225,7 @@ public function testChainAdapterPool()
$this->assertSame('cache.chain', $appCachePool->getParent());
$chainCachePool = $container->getDefinition('cache.chain');
- $this->assertNotInstanceOf(ChildDefinition::class, $chainCachePool);
+ $this->assertInstanceOf(ChildDefinition::class, $chainCachePool);
$this->assertCount(2, $chainCachePool->getArgument(0));
$this->assertInstanceOf(ChildDefinition::class, $chainCachePool->getArgument(0)[0]);
$this->assertSame('cache.adapter.array', $chainCachePool->getArgument(0)[0]->getParent());
diff --git a/src/Symfony/Component/Cache/Traits/Relay/BgsaveTrait.php b/src/Symfony/Component/Cache/Traits/Relay/BgsaveTrait.php
new file mode 100644
index 0000000000000..367f82f7bb2b6
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/Relay/BgsaveTrait.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\Cache\Traits\Relay;
+
+if (version_compare(phpversion('relay'), '0.11', '>=')) {
+ /**
+ * @internal
+ */
+ trait BgsaveTrait
+ {
+ public function bgsave($arg = null): \Relay\Relay|bool
+ {
+ return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgsave(...\func_get_args());
+ }
+ }
+} else {
+ /**
+ * @internal
+ */
+ trait BgsaveTrait
+ {
+ public function bgsave($schedule = false): \Relay\Relay|bool
+ {
+ return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgsave(...\func_get_args());
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/RelayProxy.php b/src/Symfony/Component/Cache/Traits/RelayProxy.php
index e0ca8873a0182..b6d48dd543dba 100644
--- a/src/Symfony/Component/Cache/Traits/RelayProxy.php
+++ b/src/Symfony/Component/Cache/Traits/RelayProxy.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Traits;
+use Symfony\Component\Cache\Traits\Relay\BgsaveTrait;
use Symfony\Component\Cache\Traits\Relay\CopyTrait;
use Symfony\Component\Cache\Traits\Relay\GeosearchTrait;
use Symfony\Component\Cache\Traits\Relay\GetrangeTrait;
@@ -31,6 +32,7 @@ class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
*/
class RelayProxy extends \Relay\Relay implements ResetInterface, LazyObjectInterface
{
+ use BgsaveTrait;
use CopyTrait;
use GeosearchTrait;
use GetrangeTrait;
@@ -338,11 +340,6 @@ public function lcs($key1, $key2, $options = null): mixed
return $this->initializeLazyObject()->lcs(...\func_get_args());
}
- public function bgsave($schedule = false): \Relay\Relay|bool
- {
- return $this->initializeLazyObject()->bgsave(...\func_get_args());
- }
-
public function save(): \Relay\Relay|bool
{
return $this->initializeLazyObject()->save(...\func_get_args());
diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php
index 78d885d2597a9..3d5c48f525965 100644
--- a/src/Symfony/Component/Console/Application.php
+++ b/src/Symfony/Component/Console/Application.php
@@ -1277,7 +1277,7 @@ private function splitStringByWidth(string $string, int $width): array
foreach (preg_split('//u', $m[0]) as $char) {
// test if $char could be appended to current line
- if (mb_strwidth($line.$char, 'utf8') <= $width) {
+ if (Helper::width($line.$char) <= $width) {
$line .= $char;
continue;
}
diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php
index 3981bbf3ab1ba..f10676685c14c 100644
--- a/src/Symfony/Component/Console/Helper/Helper.php
+++ b/src/Symfony/Component/Console/Helper/Helper.php
@@ -42,7 +42,9 @@ public static function width(?string $string): int
$string ??= '';
if (preg_match('//u', $string)) {
- return (new UnicodeString($string))->width(false);
+ $string = preg_replace('/[\p{Cc}\x7F]++/u', '', $string, -1, $count);
+
+ return (new UnicodeString($string))->width(false) + $count;
}
if (false === $encoding = mb_detect_encoding($string, null, true)) {
diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php
index 9ff73d2cc371a..9cd28ec771fba 100644
--- a/src/Symfony/Component/Console/Helper/Table.php
+++ b/src/Symfony/Component/Console/Helper/Table.php
@@ -558,10 +558,7 @@ private function renderCell(array $row, int $column, string $cellFormat): string
}
// str_pad won't work properly with multi-byte strings, we need to fix the padding
- if (false !== $encoding = mb_detect_encoding($cell, null, true)) {
- $width += \strlen($cell) - mb_strwidth($cell, $encoding);
- }
-
+ $width += \strlen($cell) - Helper::width($cell) - substr_count($cell, "\0");
$style = $this->getColumnStyle($column);
if ($cell instanceof TableSeparator) {
@@ -626,8 +623,48 @@ private function buildTableRows(array $rows): TableRows
foreach ($rows[$rowKey] as $column => $cell) {
$colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
- if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
- $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
+ $minWrappedWidth = 0;
+ $widthApplied = [];
+ $lengthColumnBorder = $this->getColumnSeparatorWidth() + Helper::width($this->style->getCellRowContentFormat()) - 2;
+ for ($i = $column; $i < ($column + $colspan); ++$i) {
+ if (isset($this->columnMaxWidths[$i])) {
+ $minWrappedWidth += $this->columnMaxWidths[$i];
+ $widthApplied[] = ['type' => 'max', 'column' => $i];
+ } elseif (($this->columnWidths[$i] ?? 0) > 0 && $colspan > 1) {
+ $minWrappedWidth += $this->columnWidths[$i];
+ $widthApplied[] = ['type' => 'min', 'column' => $i];
+ }
+ }
+ if (1 === \count($widthApplied)) {
+ if ($colspan > 1) {
+ $minWrappedWidth *= $colspan; // previous logic
+ }
+ } elseif (\count($widthApplied) > 1) {
+ $minWrappedWidth += (\count($widthApplied) - 1) * $lengthColumnBorder;
+ }
+
+ $cellWidth = Helper::width(Helper::removeDecoration($formatter, $cell));
+ if ($minWrappedWidth && $cellWidth > $minWrappedWidth) {
+ $cell = $formatter->formatAndWrap($cell, $minWrappedWidth);
+ }
+ // update minimal columnWidths for spanned columns
+ if ($colspan > 1 && $minWrappedWidth > 0) {
+ $columnsMinWidthProcessed = [];
+ $cellWidth = min($cellWidth, $minWrappedWidth);
+ foreach ($widthApplied as $item) {
+ if ('max' === $item['type'] && $cellWidth >= $this->columnMaxWidths[$item['column']]) {
+ $minWidthColumn = $this->columnMaxWidths[$item['column']];
+ $this->columnWidths[$item['column']] = $minWidthColumn;
+ $columnsMinWidthProcessed[$item['column']] = true;
+ $cellWidth -= $minWidthColumn + $lengthColumnBorder;
+ }
+ }
+ for ($i = $column; $i < ($column + $colspan); ++$i) {
+ if (isset($columnsMinWidthProcessed[$i])) {
+ continue;
+ }
+ $this->columnWidths[$i] = $cellWidth + $lengthColumnBorder;
+ }
}
if (!str_contains($cell ?? '', "\n")) {
continue;
diff --git a/src/Symfony/Component/Console/Tests/Helper/TableTest.php b/src/Symfony/Component/Console/Tests/Helper/TableTest.php
index 608d23c210bef..bb1b96346b604 100644
--- a/src/Symfony/Component/Console/Tests/Helper/TableTest.php
+++ b/src/Symfony/Component/Console/Tests/Helper/TableTest.php
@@ -1294,9 +1294,9 @@ public static function renderSetTitle()
'footer',
'default',
<<<'TABLE'
-+---------------+---- Multiline
++---------------+--- Multiline
header
-here -+------------------+
+here +------------------+
| ISBN | Title | Author |
+---------------+--------------------------+------------------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri |
@@ -1576,17 +1576,17 @@ public function testWithColspanAndMaxWith()
$expected =
<<
getOutputContent($output)
);
}
+
+ public function testGithubIssue60038WidthOfCellWithEmoji()
+ {
+ $table = (new Table($output = $this->getOutputStream()))
+ ->setHeaderTitle('Test Title')
+ ->setHeaders(['Title', 'Author'])
+ ->setRows([
+ ["🎭 💫 ☯"." Divine Comedy", "Dante Alighieri"],
+ // the snowflake (e2 9d 84 ef b8 8f) has a variant selector
+ ["👑 ❄️ 🗡"." Game of Thrones", "George R.R. Martin"],
+ // the snowflake in text style (e2 9d 84 ef b8 8e) has a variant selector
+ ["❄︎❄︎❄︎ snowflake in text style ❄︎❄︎❄︎", ""],
+ ["And a very long line to show difference in previous lines", ""],
+ ])
+ ;
+ $table->render();
+
+ $this->assertSame(<<
getOutputContent($output)
+ );
+ }
}
diff --git a/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php b/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php
index 7a677ebbd4e20..3e87186432efa 100644
--- a/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php
+++ b/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php
@@ -40,22 +40,22 @@ public function __get(mixed $name): mixed
}
if (isset($this->initializer)) {
- $this->service = ($this->initializer)();
+ if (\is_string($service = ($this->initializer)())) {
+ $service = (new \ReflectionClass($service))->newInstanceWithoutConstructor();
+ }
+ $this->service = $service;
unset($this->initializer);
}
return $this->service;
}
- public static function getCode(string $initializer, array $callable, Definition $definition, ContainerBuilder $container, ?string $id): string
+ public static function getCode(string $initializer, array $callable, string $class, ContainerBuilder $container, ?string $id): string
{
$method = $callable[1];
- $asClosure = 'Closure' === ($definition->getClass() ?: 'Closure');
- if ($asClosure) {
+ if ($asClosure = 'Closure' === $class) {
$class = ($callable[0] instanceof Reference ? $container->findDefinition($callable[0]) : $callable[0])->getClass();
- } else {
- $class = $definition->getClass();
}
$r = $container->getReflectionClass($class);
diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php b/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
index 2e649bdeaaadd..6b1a94dd3dd35 100644
--- a/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
+++ b/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
@@ -20,8 +20,8 @@
class AsTaggedItem
{
/**
- * @param string|null $index The property or method to use to index the item in the locator
- * @param int|null $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the locator
+ * @param string|null $index The property or method to use to index the item in the iterator/locator
+ * @param int|null $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the iterator/locator
*/
public function __construct(
public ?string $index = null,
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php
index de4acb258c3a9..52af43f606256 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php
@@ -69,6 +69,9 @@ public function process(ContainerBuilder $container): void
if (!$this->graph->hasNode($id)) {
continue;
}
+ if ($definition->isPublic()) {
+ $this->connectedIds[$id] = true;
+ }
foreach ($this->graph->getNode($id)->getOutEdges() as $edge) {
if (isset($notInlinedIds[$edge->getSourceNode()->getId()])) {
$this->currentId = $id;
@@ -188,17 +191,13 @@ private function isInlineableDefinition(string $id, Definition $definition): boo
return true;
}
- if ($definition->isPublic()) {
+ if ($definition->isPublic()
+ || $this->currentId === $id
+ || !$this->graph->hasNode($id)
+ ) {
return false;
}
- if (!$this->graph->hasNode($id)) {
- return true;
- }
-
- if ($this->currentId === $id) {
- return false;
- }
$this->connectedIds[$id] = true;
$srcIds = [];
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
index 77a1d7ef8ffc2..e3a4eba275a75 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
@@ -87,8 +87,7 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam
if (null === $index && null === $defaultIndex && $defaultPriorityMethod && $class) {
$defaultIndex = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem);
}
- $decorated = $definition->getTag('container.decorator')[0]['id'] ?? null;
- $index = $index ?? $defaultIndex ?? $defaultIndex = $decorated ?? $serviceId;
+ $index ??= $defaultIndex ??= $definition->getTag('container.decorator')[0]['id'] ?? $serviceId;
$services[] = [$priority, ++$i, $index, $serviceId, $class];
}
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
index 81c14ac5cc4d0..eedc0f484243c 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
@@ -54,17 +54,41 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
$value->setClass(ServiceLocator::class);
}
- $services = $value->getArguments()[0] ?? null;
+ $values = $value->getArguments()[0] ?? null;
+ $services = [];
- if ($services instanceof TaggedIteratorArgument) {
- $services = $this->findAndSortTaggedServices($services, $this->container);
- }
-
- if (!\is_array($services)) {
+ if ($values instanceof TaggedIteratorArgument) {
+ foreach ($this->findAndSortTaggedServices($values, $this->container) as $k => $v) {
+ $services[$k] = new ServiceClosureArgument($v);
+ }
+ } elseif (!\is_array($values)) {
throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set.', $this->currentId));
+ } else {
+ $i = 0;
+
+ foreach ($values as $k => $v) {
+ if ($v instanceof ServiceClosureArgument) {
+ $services[$k] = $v;
+ continue;
+ }
+
+ if ($i === $k) {
+ if ($v instanceof Reference) {
+ $k = (string) $v;
+ }
+ ++$i;
+ } elseif (\is_int($k)) {
+ $i = null;
+ }
+
+ $services[$k] = new ServiceClosureArgument($v);
+ }
+ if (\count($services) === $i) {
+ ksort($services);
+ }
}
- $value->setArgument(0, self::map($services));
+ $value->setArgument(0, $services);
$id = '.service_locator.'.ContainerBuilder::hash($value);
@@ -83,8 +107,12 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
public static function register(ContainerBuilder $container, array $map, ?string $callerId = null): Reference
{
+ foreach ($map as $k => $v) {
+ $map[$k] = new ServiceClosureArgument($v);
+ }
+
$locator = (new Definition(ServiceLocator::class))
- ->addArgument(self::map($map))
+ ->addArgument($map)
->addTag('container.service_locator');
if (null !== $callerId && $container->hasDefinition($callerId)) {
@@ -109,29 +137,4 @@ public static function register(ContainerBuilder $container, array $map, ?string
return new Reference($id);
}
-
- public static function map(array $services): array
- {
- $i = 0;
-
- foreach ($services as $k => $v) {
- if ($v instanceof ServiceClosureArgument) {
- continue;
- }
-
- if ($i === $k) {
- if ($v instanceof Reference) {
- unset($services[$k]);
- $k = (string) $v;
- }
- ++$i;
- } elseif (\is_int($k)) {
- $i = null;
- }
-
- $services[$k] = new ServiceClosureArgument($v);
- }
-
- return $services;
- }
}
diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
index 7389ca6310447..e978cac41e885 100644
--- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
+++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
@@ -791,10 +791,11 @@ public function parameterCannotBeEmpty(string $name, string $message): void
* * The parameter bag is frozen;
* * Extension loading is disabled.
*
- * @param bool $resolveEnvPlaceholders Whether %env()% parameters should be resolved using the current
- * env vars or be replaced by uniquely identifiable placeholders.
- * Set to "true" when you want to use the current ContainerBuilder
- * directly, keep to "false" when the container is dumped instead.
+ * @param bool $resolveEnvPlaceholders Whether %env()% parameters should be resolved at build time using
+ * the current env var values (true), or be resolved at runtime based
+ * on the environment (false). In general, this should be set to "true"
+ * when you want to use the current ContainerBuilder directly, and to
+ * "false" when the container is dumped instead.
*/
public function compile(bool $resolveEnvPlaceholders = false): void
{
@@ -1109,14 +1110,15 @@ private function createService(Definition $definition, array &$inlineServices, b
}
if (\is_array($callable) && (
- $callable[0] instanceof Reference
+ 'Closure' !== $class
+ || $callable[0] instanceof Reference
|| $callable[0] instanceof Definition && !isset($inlineServices[spl_object_hash($callable[0])])
)) {
$initializer = function () use ($callable, &$inlineServices) {
return $this->doResolveServices($callable[0], $inlineServices);
};
- $proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $definition, $this, $id).';');
+ $proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $class, $this, $id).';');
$this->shareService($definition, $proxy, $id, $inlineServices);
return $proxy;
diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
index ee7e519a0c8aa..9568ad26b349c 100644
--- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
+++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
@@ -1184,13 +1184,13 @@ private function addNewInstance(Definition $definition, string $return = '', ?st
throw new RuntimeException(\sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a'));
}
- if (['...'] === $arguments && ($definition->isLazy() || 'Closure' !== ($definition->getClass() ?? 'Closure')) && (
+ if (['...'] === $arguments && ('Closure' !== ($class = $definition->getClass() ?: 'Closure') || $definition->isLazy() && (
$callable[0] instanceof Reference
|| ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0]))
- )) {
+ ))) {
$initializer = 'fn () => '.$this->dumpValue($callable[0]);
- return $return.LazyClosure::getCode($initializer, $callable, $definition, $this->container, $id).$tail;
+ return $return.LazyClosure::getCode($initializer, $callable, $class, $this->container, $id).$tail;
}
if ($callable[0] instanceof Reference
diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
index ec115500bb0cf..d79e7b90408b2 100644
--- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
+++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
@@ -50,18 +50,18 @@ public function dump(array $options = []): string
$this->dumper ??= new YmlDumper();
- return $this->container->resolveEnvPlaceholders($this->addParameters()."\n".$this->addServices());
+ return $this->addParameters()."\n".$this->addServices();
}
private function addService(string $id, Definition $definition): string
{
- $code = " $id:\n";
+ $code = " {$this->dumper->dump($id)}:\n";
if ($class = $definition->getClass()) {
if (str_starts_with($class, '\\')) {
$class = substr($class, 1);
}
- $code .= \sprintf(" class: %s\n", $this->dumper->dump($class));
+ $code .= \sprintf(" class: %s\n", $this->dumper->dump($this->container->resolveEnvPlaceholders($class)));
}
if (!$definition->isPrivate()) {
@@ -87,7 +87,7 @@ private function addService(string $id, Definition $definition): string
}
if ($definition->getFile()) {
- $code .= \sprintf(" file: %s\n", $this->dumper->dump($definition->getFile()));
+ $code .= \sprintf(" file: %s\n", $this->dumper->dump($this->container->resolveEnvPlaceholders($definition->getFile())));
}
if ($definition->isSynthetic()) {
@@ -238,7 +238,7 @@ private function dumpCallable(mixed $callable): mixed
}
}
- return $callable;
+ return $this->container->resolveEnvPlaceholders($callable);
}
/**
@@ -299,7 +299,7 @@ private function dumpValue(mixed $value): mixed
if (\is_array($value)) {
$code = [];
foreach ($value as $k => $v) {
- $code[$k] = $this->dumpValue($v);
+ $code[$this->container->resolveEnvPlaceholders($k)] = $this->dumpValue($v);
}
return $code;
@@ -319,7 +319,7 @@ private function dumpValue(mixed $value): mixed
throw new RuntimeException(\sprintf('Unable to dump a service container if a parameter is an object or a resource, got "%s".', get_debug_type($value)));
}
- return $value;
+ return $this->container->resolveEnvPlaceholders($value);
}
private function getServiceCall(string $id, ?Reference $reference = null): string
@@ -359,7 +359,7 @@ private function prepareParameters(array $parameters, bool $escape = true): arra
$filtered[$key] = $value;
}
- return $escape ? $this->escape($filtered) : $filtered;
+ return $escape ? $this->container->resolveEnvPlaceholders($this->escape($filtered)) : $filtered;
}
private function escape(array $arguments): array
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php b/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php
index 46ef1591785cf..9652a86fd24b6 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php
@@ -34,7 +34,7 @@ public function testThrowsWhenNotUsingInterface()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cannot create adapter for service "foo" because "Symfony\Component\DependencyInjection\Tests\Argument\LazyClosureTest" is not an interface.');
- LazyClosure::getCode('foo', [new \stdClass(), 'bar'], new Definition(LazyClosureTest::class), new ContainerBuilder(), 'foo');
+ LazyClosure::getCode('foo', [new \stdClass(), 'bar'], LazyClosureTest::class, new ContainerBuilder(), 'foo');
}
public function testThrowsOnNonFunctionalInterface()
@@ -42,7 +42,7 @@ public function testThrowsOnNonFunctionalInterface()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cannot create adapter for service "foo" because interface "Symfony\Component\DependencyInjection\Tests\Argument\NonFunctionalInterface" doesn\'t have exactly one method.');
- LazyClosure::getCode('foo', [new \stdClass(), 'bar'], new Definition(NonFunctionalInterface::class), new ContainerBuilder(), 'foo');
+ LazyClosure::getCode('foo', [new \stdClass(), 'bar'], NonFunctionalInterface::class, new ContainerBuilder(), 'foo');
}
public function testThrowsOnUnknownMethodInInterface()
@@ -50,7 +50,7 @@ public function testThrowsOnUnknownMethodInInterface()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cannot create lazy closure for service "bar" because its corresponding callable is invalid.');
- LazyClosure::getCode('bar', [new Definition(FunctionalInterface::class), 'bar'], new Definition(\Closure::class), new ContainerBuilder(), 'bar');
+ LazyClosure::getCode('bar', [new Definition(FunctionalInterface::class), 'bar'], \Closure::class, new ContainerBuilder(), 'bar');
}
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php
index 812b47c7a6f1f..9a93067756d50 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php
@@ -86,6 +86,26 @@ public function testProcessValue()
$this->assertSame(CustomDefinition::class, \get_class($locator('inlines.service')));
}
+ public function testServiceListIsOrdered()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('bar', CustomDefinition::class);
+ $container->register('baz', CustomDefinition::class);
+
+ $container->register('foo', ServiceLocator::class)
+ ->setArguments([[
+ new Reference('baz'),
+ new Reference('bar'),
+ ]])
+ ->addTag('container.service_locator')
+ ;
+
+ (new ServiceLocatorTagPass())->process($container);
+
+ $this->assertSame(['bar', 'baz'], array_keys($container->getDefinition('foo')->getArgument(0)));
+ }
+
public function testServiceWithKeyOverwritesPreviousInheritedKey()
{
$container = new ContainerBuilder();
@@ -170,6 +190,27 @@ public function testTaggedServices()
$this->assertSame(TestDefinition2::class, $locator('baz')::class);
}
+ public function testTaggedServicesKeysAreKept()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('bar', TestDefinition1::class)->addTag('test_tag', ['index' => 0]);
+ $container->register('baz', TestDefinition2::class)->addTag('test_tag', ['index' => 1]);
+
+ $container->register('foo', ServiceLocator::class)
+ ->setArguments([new TaggedIteratorArgument('test_tag', 'index', null, true)])
+ ->addTag('container.service_locator')
+ ;
+
+ (new ServiceLocatorTagPass())->process($container);
+
+ /** @var ServiceLocator $locator */
+ $locator = $container->get('foo');
+
+ $this->assertSame(TestDefinition1::class, $locator(0)::class);
+ $this->assertSame(TestDefinition2::class, $locator(1)::class);
+ }
+
public function testIndexedByServiceIdWithDecoration()
{
$container = new ContainerBuilder();
@@ -201,15 +242,33 @@ public function testIndexedByServiceIdWithDecoration()
static::assertInstanceOf(DecoratedService::class, $locator->get(Service::class));
}
- public function testDefinitionOrderIsTheSame()
+ public function testServicesKeysAreKept()
{
$container = new ContainerBuilder();
$container->register('service-1');
$container->register('service-2');
+ $container->register('service-3');
$locator = ServiceLocatorTagPass::register($container, [
- new Reference('service-2'),
new Reference('service-1'),
+ 'service-2' => new Reference('service-2'),
+ 'foo' => new Reference('service-3'),
+ ]);
+ $locator = $container->getDefinition($locator);
+ $factories = $locator->getArguments()[0];
+
+ static::assertSame([0, 'service-2', 'foo'], array_keys($factories));
+ }
+
+ public function testDefinitionOrderIsTheSame()
+ {
+ $container = new ContainerBuilder();
+ $container->register('service-1');
+ $container->register('service-2');
+
+ $locator = ServiceLocatorTagPass::register($container, [
+ 'service-2' => new Reference('service-2'),
+ 'service-1' => new Reference('service-1'),
]);
$locator = $container->getDefinition($locator);
$factories = $locator->getArguments()[0];
diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
index 544303bbe859a..73be8aaedeaba 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
@@ -48,6 +48,7 @@
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\Tests\Compiler\Foo;
+use Symfony\Component\DependencyInjection\Tests\Compiler\MyCallable;
use Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface;
use Symfony\Component\DependencyInjection\Tests\Compiler\Wither;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
@@ -530,6 +531,19 @@ public function testClosureProxy()
$this->assertInstanceOf(Foo::class, $container->get('closure_proxy')->theMethod());
}
+ public function testClosureProxyWithStaticMethod()
+ {
+ $container = new ContainerBuilder();
+ $container->register('closure_proxy', SingleMethodInterface::class)
+ ->setPublic('true')
+ ->setFactory(['Closure', 'fromCallable'])
+ ->setArguments([[MyCallable::class, 'theMethodImpl']]);
+ $container->compile();
+
+ $this->assertInstanceOf(SingleMethodInterface::class, $container->get('closure_proxy'));
+ $this->assertSame(124, $container->get('closure_proxy')->theMethod());
+ }
+
public function testCreateServiceClass()
{
$builder = new ContainerBuilder();
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
index f9ff3fff786a3..3a21d7aa9a9c5 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
@@ -215,6 +215,26 @@ public function testDumpNonScalarTags()
$this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/services_with_array_tags.yml'), $dumper->dump());
}
+ public function testDumpResolvedEnvPlaceholders()
+ {
+ $container = new ContainerBuilder();
+ $container->setParameter('%env(PARAMETER_NAME)%', '%env(PARAMETER_VALUE)%');
+ $container
+ ->register('service', '%env(SERVICE_CLASS)%')
+ ->setFile('%env(SERVICE_FILE)%')
+ ->addArgument('%env(SERVICE_ARGUMENT)%')
+ ->setProperty('%env(SERVICE_PROPERTY_NAME)%', '%env(SERVICE_PROPERTY_VALUE)%')
+ ->addMethodCall('%env(SERVICE_METHOD_NAME)%', ['%env(SERVICE_METHOD_ARGUMENT)%'])
+ ->setFactory('%env(SERVICE_FACTORY)%')
+ ->setConfigurator('%env(SERVICE_CONFIGURATOR)%')
+ ->setPublic(true)
+ ;
+ $container->compile();
+ $dumper = new YamlDumper($container);
+
+ $this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/container_with_env_placeholders.yml'), $dumper->dump());
+ }
+
private function assertEqualYamlStructure(string $expected, string $yaml, string $message = '')
{
$parser = new Parser();
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/StaticConstructorAutoconfigure.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/StaticConstructorAutoconfigure.php
index 3d42a8c770952..09479fe55d2ae 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/StaticConstructorAutoconfigure.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/StaticConstructorAutoconfigure.php
@@ -12,7 +12,6 @@
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
-use Symfony\Component\DependencyInjection\Attribute\Factory;
#[Autoconfigure(bind: ['$foo' => 'foo'], constructor: 'create')]
class StaticConstructorAutoconfigure
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
index 7349cb1a076d8..98cbbec909751 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
@@ -461,6 +461,11 @@ class MyCallable
public function __invoke(): void
{
}
+
+ public static function theMethodImpl(): int
+ {
+ return 124;
+ }
}
class MyInlineService
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 216dca434e489..ccd8d2e0bf63b 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()); } });
}
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/container_with_env_placeholders.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/container_with_env_placeholders.yml
new file mode 100644
index 0000000000000..46c91130faecd
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/container_with_env_placeholders.yml
@@ -0,0 +1,19 @@
+parameters:
+ '%env(PARAMETER_NAME)%': '%env(PARAMETER_VALUE)%'
+
+services:
+ service_container:
+ class: Symfony\Component\DependencyInjection\ContainerInterface
+ public: true
+ synthetic: true
+ service:
+ class: '%env(SERVICE_CLASS)%'
+ public: true
+ file: '%env(SERVICE_FILE)%'
+ arguments: ['%env(SERVICE_ARGUMENT)%']
+ properties: { '%env(SERVICE_PROPERTY_NAME)%': '%env(SERVICE_PROPERTY_VALUE)%' }
+ calls:
+ - ['%env(SERVICE_METHOD_NAME)%', ['%env(SERVICE_METHOD_ARGUMENT)%']]
+
+ factory: '%env(SERVICE_FACTORY)%'
+ configurator: '%env(SERVICE_CONFIGURATOR)%'
diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php
index e41875e268203..7550da83d4fdb 100644
--- a/src/Symfony/Component/DomCrawler/Crawler.php
+++ b/src/Symfony/Component/DomCrawler/Crawler.php
@@ -747,12 +747,12 @@ public function selectImage(string $value): static
}
/**
- * Selects a button by name or alt value for images.
+ * Selects a button by its text content, id, value, name or alt attribute.
*/
public function selectButton(string $value): static
{
return $this->filterRelativeXPath(
- \sprintf('descendant-or-self::input[((contains(%1$s, "submit") or contains(%1$s, "button")) and contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s)) or (contains(%1$s, "image") and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %2$s)) or @id=%3$s or @name=%3$s] | descendant-or-self::button[contains(concat(\' \', normalize-space(string(.)), \' \'), %2$s) or @id=%3$s or @name=%3$s]', 'translate(@type, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")', static::xpathLiteral(' '.$value.' '), static::xpathLiteral($value))
+ \sprintf('descendant-or-self::input[((contains(%1$s, "submit") or contains(%1$s, "button")) and contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s)) or (contains(%1$s, "image") and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %2$s)) or @id=%3$s or @name=%3$s] | descendant-or-self::button[contains(concat(\' \', normalize-space(string(.)), \' \'), %2$s) or contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s) or @id=%3$s or @name=%3$s]', 'translate(@type, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")', static::xpathLiteral(' '.$value.' '), static::xpathLiteral($value))
);
}
diff --git a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php
index 5cdbbbf45870d..53169efcab8e5 100644
--- a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php
+++ b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php
@@ -452,10 +452,10 @@ public function testFilterXpathComplexQueries()
$this->assertCount(0, $crawler->filterXPath('/body'));
$this->assertCount(1, $crawler->filterXPath('./body'));
$this->assertCount(1, $crawler->filterXPath('.//body'));
- $this->assertCount(5, $crawler->filterXPath('.//input'));
+ $this->assertCount(6, $crawler->filterXPath('.//input'));
$this->assertCount(4, $crawler->filterXPath('//form')->filterXPath('//button | //input'));
$this->assertCount(1, $crawler->filterXPath('body'));
- $this->assertCount(6, $crawler->filterXPath('//button | //input'));
+ $this->assertCount(8, $crawler->filterXPath('//button | //input'));
$this->assertCount(1, $crawler->filterXPath('//body'));
$this->assertCount(1, $crawler->filterXPath('descendant-or-self::body'));
$this->assertCount(1, $crawler->filterXPath('//div[@id="parent"]')->filterXPath('./div'), 'A child selection finds only the current div');
@@ -723,16 +723,23 @@ public function testSelectButton()
$this->assertNotSame($crawler, $crawler->selectButton('FooValue'), '->selectButton() returns a new instance of a crawler');
$this->assertInstanceOf(Crawler::class, $crawler->selectButton('FooValue'), '->selectButton() returns a new instance of a crawler');
- $this->assertEquals(1, $crawler->selectButton('FooValue')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('FooName')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('FooId')->count(), '->selectButton() selects buttons');
+ $this->assertCount(1, $crawler->selectButton('FooValue'), '->selectButton() selects type-submit inputs by value');
+ $this->assertCount(1, $crawler->selectButton('FooName'), '->selectButton() selects type-submit inputs by name');
+ $this->assertCount(1, $crawler->selectButton('FooId'), '->selectButton() selects type-submit inputs by id');
- $this->assertEquals(1, $crawler->selectButton('BarValue')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('BarName')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('BarId')->count(), '->selectButton() selects buttons');
+ $this->assertCount(1, $crawler->selectButton('BarValue'), '->selectButton() selects type-button inputs by value');
+ $this->assertCount(1, $crawler->selectButton('BarName'), '->selectButton() selects type-button inputs by name');
+ $this->assertCount(1, $crawler->selectButton('BarId'), '->selectButton() selects type-button inputs by id');
- $this->assertEquals(1, $crawler->selectButton('FooBarValue')->count(), '->selectButton() selects buttons with form attribute too');
- $this->assertEquals(1, $crawler->selectButton('FooBarName')->count(), '->selectButton() selects buttons with form attribute too');
+ $this->assertCount(1, $crawler->selectButton('ImageAlt'), '->selectButton() selects type-image inputs by alt');
+
+ $this->assertCount(1, $crawler->selectButton('ButtonValue'), '->selectButton() selects buttons by value');
+ $this->assertCount(1, $crawler->selectButton('ButtonName'), '->selectButton() selects buttons by name');
+ $this->assertCount(1, $crawler->selectButton('ButtonId'), '->selectButton() selects buttons by id');
+ $this->assertCount(1, $crawler->selectButton('ButtonText'), '->selectButton() selects buttons by text content');
+
+ $this->assertCount(1, $crawler->selectButton('FooBarValue'), '->selectButton() selects buttons with form attribute too');
+ $this->assertCount(1, $crawler->selectButton('FooBarName'), '->selectButton() selects buttons with form attribute too');
}
public function testSelectButtonWithSingleQuotesInNameAttribute()
@@ -1322,6 +1329,9 @@ public function createTestCrawler($uri = null)
+
+
+
One
Two
diff --git a/src/Symfony/Component/ErrorHandler/Debug.php b/src/Symfony/Component/ErrorHandler/Debug.php
index d54a38c4cac12..b090040d024b4 100644
--- a/src/Symfony/Component/ErrorHandler/Debug.php
+++ b/src/Symfony/Component/ErrorHandler/Debug.php
@@ -20,7 +20,7 @@ class Debug
{
public static function enable(): ErrorHandler
{
- error_reporting(-1);
+ error_reporting(\E_ALL & ~\E_DEPRECATED & ~\E_USER_DEPRECATED);
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
ini_set('display_errors', 0);
diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
index b03f8da4444fe..496c48bc12c9a 100644
--- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
+++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
@@ -39,14 +39,14 @@ public function __construct(
/**
* Transforms a normalized format into a localized money string.
*
- * @param int|float|null $value Normalized number
+ * @param int|float|string|null $value Normalized number
*
* @throws TransformationFailedException if the given value is not numeric or
* if the value cannot be transformed
*/
public function transform(mixed $value): string
{
- if (null !== $value && 1 !== $this->divisor) {
+ if (null !== $value && '' !== $value && 1 !== $this->divisor) {
if (!is_numeric($value)) {
throw new TransformationFailedException('Expected a numeric.');
}
diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
index 3020dd1483c28..ffcbc1feee6d7 100644
--- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
+++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
@@ -41,14 +41,14 @@ public function __construct(
/**
* Transforms a number type into localized number.
*
- * @param int|float|null $value Number value
+ * @param int|float|string|null $value Number value
*
* @throws TransformationFailedException if the given value is not numeric
* or if the value cannot be transformed
*/
public function transform(mixed $value): string
{
- if (null === $value) {
+ if (null === $value || '' === $value) {
return '';
}
diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php
index 299f919373403..a7da65bdb60fa 100644
--- a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php
+++ b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php
@@ -199,7 +199,13 @@ public function onSubmit(FormEvent $event): void
}
if ($this->keepAsList) {
- $formReindex = [];
+ $formReindex = $dataKeys = [];
+ foreach ($data as $key => $value) {
+ $dataKeys[] = $key;
+ }
+ foreach ($dataKeys as $key) {
+ unset($data[$key]);
+ }
foreach ($form as $name => $child) {
$formReindex[] = $child;
$form->remove($name);
@@ -207,9 +213,9 @@ public function onSubmit(FormEvent $event): void
foreach ($formReindex as $index => $child) {
$form->add($index, $this->type, array_replace([
'property_path' => '['.$index.']',
- ], $this->options));
+ ], $this->options, ['data' => $child->getData()]));
+ $data[$index] = $child->getData();
}
- $data = array_values($data);
}
$event->setData($data);
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php
index e5733ad96abb5..689c6f0d4da32 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php
@@ -54,6 +54,13 @@ public function testTransformExpectsNumeric()
$transformer->transform('abcd');
}
+ public function testTransformEmptyString()
+ {
+ $transformer = new MoneyToLocalizedStringTransformer(null, null, null, 100);
+
+ $this->assertSame('', $transformer->transform(''));
+ }
+
public function testTransformEmpty()
{
$transformer = new MoneyToLocalizedStringTransformer();
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php
index 37448db51030a..c0344b9f232ea 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php
@@ -49,6 +49,7 @@ public static function provideTransformations()
{
return [
[null, '', 'de_AT'],
+ ['', '', 'de_AT'],
[1, '1', 'de_AT'],
[1.5, '1,5', 'de_AT'],
[1234.5, '1234,5', 'de_AT'],
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php
index 934460c8f98a4..390f6b04a60c5 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php
@@ -310,7 +310,7 @@ public function testOnSubmitDealsWithObjectBackedIteratorAggregate()
$this->assertArrayNotHasKey(2, $event->getData());
}
- public function testOnSubmitDealsWithArrayBackedIteratorAggregate()
+ public function testOnSubmitDealsWithDoctrineCollection()
{
$this->builder->add($this->getBuilder('1'));
@@ -323,6 +323,19 @@ public function testOnSubmitDealsWithArrayBackedIteratorAggregate()
$this->assertArrayNotHasKey(2, $event->getData());
}
+ public function testKeepAsListWorksWithTraversableArrayAccess()
+ {
+ $this->builder->add($this->getBuilder('1'));
+
+ $data = new \ArrayIterator([0 => 'first', 1 => 'second', 2 => 'third']);
+ $event = new FormEvent($this->builder->getForm(), $data);
+ $listener = new ResizeFormListener(TextType::class, keepAsList: true);
+ $listener->onSubmit($event);
+
+ $this->assertCount(1, $event->getData());
+ $this->assertArrayHasKey(0, $event->getData());
+ }
+
public function testOnSubmitDeleteEmptyNotCompoundEntriesIfAllowDelete()
{
$this->builder->setData(['0' => 'first', '1' => 'second']);
diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
index a1d1a38402892..1661519b717b1 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
@@ -257,7 +257,7 @@ public function testCollectionTypeKeepAsListOptionTrue()
{
$formMetadata = new ClassMetadata(Form::class);
$authorMetadata = (new ClassMetadata(Author::class))
- ->addPropertyConstraint('firstName', new NotBlank());
+ ->addPropertyConstraint('firstName', new Length(1));
$organizationMetadata = (new ClassMetadata(Organization::class))
->addPropertyConstraint('authors', new Valid());
$metadataFactory = $this->createMock(MetadataFactoryInterface::class);
@@ -301,22 +301,22 @@ public function testCollectionTypeKeepAsListOptionTrue()
$form->submit([
'authors' => [
0 => [
- 'firstName' => '', // Fires a Not Blank Error
+ 'firstName' => 'foobar', // Fires a Length Error
'lastName' => 'lastName1',
],
// key "1" could be missing if we add 4 blank form entries and then remove it.
2 => [
- 'firstName' => '', // Fires a Not Blank Error
+ 'firstName' => 'barfoo', // Fires a Length Error
'lastName' => 'lastName3',
],
3 => [
- 'firstName' => '', // Fires a Not Blank Error
+ 'firstName' => 'barbaz', // Fires a Length Error
'lastName' => 'lastName3',
],
],
]);
- // Form does have 3 not blank errors
+ // Form does have 3 length errors
$errors = $form->getErrors(true);
$this->assertCount(3, $errors);
@@ -328,12 +328,15 @@ public function testCollectionTypeKeepAsListOptionTrue()
];
$this->assertTrue($form->get('authors')->has('0'));
+ $this->assertSame('foobar', $form->get('authors')->get('0')->getData()->firstName);
$this->assertContains('data.authors[0].firstName', $errorPaths);
$this->assertTrue($form->get('authors')->has('1'));
+ $this->assertSame('barfoo', $form->get('authors')->get('1')->getData()->firstName);
$this->assertContains('data.authors[1].firstName', $errorPaths);
$this->assertTrue($form->get('authors')->has('2'));
+ $this->assertSame('barbaz', $form->get('authors')->get('2')->getData()->firstName);
$this->assertContains('data.authors[2].firstName', $errorPaths);
$this->assertFalse($form->get('authors')->has('3'));
diff --git a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
index 05d2a7c22870e..8af4c755833bd 100644
--- a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
+++ b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
@@ -50,7 +50,7 @@ public function __construct(int $maxHostConnections, int $maxPendingPushes)
curl_multi_setopt($this->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
}
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS') && 0 < $maxHostConnections) {
- $maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections) ? 4294967295 : $maxHostConnections;
+ $maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections) ? min(50 * $maxHostConnections, 4294967295) : $maxHostConnections;
}
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
diff --git a/src/Symfony/Component/HttpClient/Tests/AmpHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/AmpHttpClientTest.php
index d03693694a746..dd45668a837d4 100644
--- a/src/Symfony/Component/HttpClient/Tests/AmpHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/AmpHttpClientTest.php
@@ -19,6 +19,14 @@
*/
class AmpHttpClientTest extends HttpClientTestCase
{
+ /**
+ * @group transient
+ */
+ public function testNonBlockingStream()
+ {
+ parent::testNonBlockingStream();
+ }
+
protected function getHttpClient(string $testCase): HttpClientInterface
{
return new AmpHttpClient(['verify_peer' => false, 'verify_host' => false, 'timeout' => 5]);
diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php
index db78105cc83cf..5ee07ecbde3bd 100644
--- a/src/Symfony/Component/HttpFoundation/Request.php
+++ b/src/Symfony/Component/HttpFoundation/Request.php
@@ -1384,7 +1384,7 @@ public function isMethodCacheable(): bool
public function getProtocolVersion(): ?string
{
if ($this->isFromTrustedProxy()) {
- preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via') ?? '', $matches);
+ preg_match('~^(HTTP/)?([1-9]\.[0-9])\b~', $this->headers->get('Via') ?? '', $matches);
if ($matches) {
return 'HTTP/'.$matches[2];
diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
index 9031fc34f52f9..220702a1d2990 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
@@ -2417,6 +2417,8 @@ public static function protocolVersionProvider()
'trusted with via and protocol name' => ['HTTP/2.0', true, 'HTTP/1.0 fred, HTTP/1.1 nowhere.com (Apache/1.1)', 'HTTP/1.0'],
'trusted with broken via' => ['HTTP/2.0', true, 'HTTP/1^0 foo', 'HTTP/2.0'],
'trusted with partially-broken via' => ['HTTP/2.0', true, '1.0 fred, foo', 'HTTP/1.0'],
+ 'trusted with simple via' => ['HTTP/2.0', true, 'HTTP/1.0', 'HTTP/1.0'],
+ 'trusted with only version via' => ['HTTP/2.0', true, '1.0', 'HTTP/1.0'],
];
}
diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
index a196250e8b23b..3a10c9d9c7854 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
@@ -232,6 +232,10 @@ private function mapRequestPayload(Request $request, ArgumentMetadata $argument,
private function mapUploadedFile(Request $request, ArgumentMetadata $argument, MapUploadedFile $attribute): UploadedFile|array|null
{
- return $request->files->get($attribute->name ?? $argument->getName(), []);
+ if (!($files = $request->files->get($attribute->name ?? $argument->getName(), [])) && ($argument->isNullable() || $argument->hasDefaultValue())) {
+ return null;
+ }
+
+ return $files;
}
}
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/CacheWasLockedException.php b/src/Symfony/Component/HttpKernel/HttpCache/CacheWasLockedException.php
new file mode 100644
index 0000000000000..f13946ad71a68
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/HttpCache/CacheWasLockedException.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\HttpCache;
+
+/**
+ * @internal
+ */
+class CacheWasLockedException extends \Exception
+{
+}
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
index 3ef1b8dcb821f..2b1be6a95a707 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
@@ -210,7 +210,13 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R
$this->record($request, 'reload');
$response = $this->fetch($request, $catch);
} else {
- $response = $this->lookup($request, $catch);
+ $response = null;
+ do {
+ try {
+ $response = $this->lookup($request, $catch);
+ } catch (CacheWasLockedException) {
+ }
+ } while (null === $response);
}
$this->restoreResponseBody($request, $response);
@@ -560,15 +566,7 @@ protected function lock(Request $request, Response $entry): bool
// wait for the lock to be released
if ($this->waitForLock($request)) {
- // replace the current entry with the fresh one
- $new = $this->lookup($request);
- $entry->headers = $new->headers;
- $entry->setContent($new->getContent());
- $entry->setStatusCode($new->getStatusCode());
- $entry->setProtocolVersion($new->getProtocolVersion());
- foreach ($new->headers->getCookies() as $cookie) {
- $entry->headers->setCookie($cookie);
- }
+ throw new CacheWasLockedException(); // unwind back to handle(), try again
} else {
// backend is slow as hell, send a 503 response (to avoid the dog pile effect)
$entry->setStatusCode(503);
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index 09b362c1ff72c..12ec6da6338f4 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -73,11 +73,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
private static array $freshCache = [];
- public const VERSION = '7.2.7';
- public const VERSION_ID = 70207;
+ public const VERSION = '7.2.8';
+ public const VERSION_ID = 70208;
public const MAJOR_VERSION = 7;
public const MINOR_VERSION = 2;
- public const RELEASE_VERSION = 7;
+ public const RELEASE_VERSION = 8;
public const EXTRA_VERSION = '';
public const END_OF_MAINTENANCE = '07/2025';
diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php
index 5eb0d32483ed5..479fbf180869c 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php
@@ -307,6 +307,66 @@ static function () {},
$resolver->onKernelControllerArguments($event);
}
+ /**
+ * @dataProvider provideContext
+ */
+ public function testShouldAllowEmptyWhenNullable(RequestPayloadValueResolver $resolver, Request $request)
+ {
+ $attribute = new MapUploadedFile();
+ $argument = new ArgumentMetadata(
+ 'qux',
+ UploadedFile::class,
+ false,
+ false,
+ null,
+ true,
+ [$attribute::class => $attribute]
+ );
+ /** @var HttpKernelInterface&MockObject $httpKernel */
+ $httpKernel = $this->createMock(HttpKernelInterface::class);
+ $event = new ControllerArgumentsEvent(
+ $httpKernel,
+ static function () {},
+ $resolver->resolve($request, $argument),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ );
+ $resolver->onKernelControllerArguments($event);
+ $data = $event->getArguments()[0];
+
+ $this->assertNull($data);
+ }
+
+ /**
+ * @dataProvider provideContext
+ */
+ public function testShouldAllowEmptyWhenHasDefaultValue(RequestPayloadValueResolver $resolver, Request $request)
+ {
+ $attribute = new MapUploadedFile();
+ $argument = new ArgumentMetadata(
+ 'qux',
+ UploadedFile::class,
+ false,
+ true,
+ 'default-value',
+ false,
+ [$attribute::class => $attribute]
+ );
+ /** @var HttpKernelInterface&MockObject $httpKernel */
+ $httpKernel = $this->createMock(HttpKernelInterface::class);
+ $event = new ControllerArgumentsEvent(
+ $httpKernel,
+ static function () {},
+ $resolver->resolve($request, $argument),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ );
+ $resolver->onKernelControllerArguments($event);
+ $data = $event->getArguments()[0];
+
+ $this->assertSame('default-value', $data);
+ }
+
public static function provideContext(): iterable
{
$resolver = new RequestPayloadValueResolver(
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
index 0a9e54899022c..2021d7a975fca 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
@@ -17,6 +17,7 @@
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\HttpCache\Esi;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
+use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Kernel;
@@ -662,6 +663,7 @@ public function testDegradationWhenCacheLocked()
*/
sleep(10);
+ $this->store = $this->createStore(); // create another store instance that does not hold the current lock
$this->request('GET', '/');
$this->assertHttpKernelIsNotCalled();
$this->assertEquals(200, $this->response->getStatusCode());
@@ -680,6 +682,64 @@ public function testDegradationWhenCacheLocked()
$this->assertEquals('Old response', $this->response->getContent());
}
+ public function testHitBackendOnlyOnceWhenCacheWasLocked()
+ {
+ // Disable stale-while-revalidate, it circumvents waiting for the lock
+ $this->cacheConfig['stale_while_revalidate'] = 0;
+
+ $this->setNextResponses([
+ [
+ 'status' => 200,
+ 'body' => 'initial response',
+ 'headers' => [
+ 'Cache-Control' => 'public, no-cache',
+ 'Last-Modified' => 'some while ago',
+ ],
+ ],
+ [
+ 'status' => 304,
+ 'body' => '',
+ 'headers' => [
+ 'Cache-Control' => 'public, no-cache',
+ 'Last-Modified' => 'some while ago',
+ ],
+ ],
+ [
+ 'status' => 500,
+ 'body' => 'The backend should not be called twice during revalidation',
+ 'headers' => [],
+ ],
+ ]);
+
+ $this->request('GET', '/'); // warm the cache
+
+ // Use a store that simulates a cache entry being locked upon first attempt
+ $this->store = new class(sys_get_temp_dir() . '/http_cache') extends Store {
+ private bool $hasLock = false;
+
+ public function lock(Request $request): bool
+ {
+ $hasLock = $this->hasLock;
+ $this->hasLock = true;
+
+ return $hasLock;
+ }
+
+ public function isLocked(Request $request): bool
+ {
+ return false;
+ }
+ };
+
+ $this->request('GET', '/'); // hit the cache with simulated lock/concurrency block
+
+ $this->assertEquals(200, $this->response->getStatusCode());
+ $this->assertEquals('initial response', $this->response->getContent());
+
+ $traces = $this->cache->getTraces();
+ $this->assertSame(['stale', 'valid', 'store'], current($traces));
+ }
+
public function testHitsCachedResponseWithSMaxAgeDirective()
{
$time = \DateTimeImmutable::createFromFormat('U', time() - 5);
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
index 26a29f16b2b75..88f6bed56f4cf 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
@@ -30,7 +30,7 @@ abstract class HttpCacheTestCase extends TestCase
protected $responses;
protected $catch;
protected $esi;
- protected Store $store;
+ protected ?Store $store = null;
protected function setUp(): void
{
@@ -115,7 +115,9 @@ public function request($method, $uri = '/', $server = [], $cookies = [], $esi =
$this->kernel->reset();
- $this->store = new Store(sys_get_temp_dir().'/http_cache');
+ if (! $this->store) {
+ $this->store = $this->createStore();
+ }
if (!isset($this->cacheConfig['debug'])) {
$this->cacheConfig['debug'] = true;
@@ -183,4 +185,9 @@ public static function clearDirectory($directory)
closedir($fp);
}
+
+ protected function createStore(): Store
+ {
+ return new Store(sys_get_temp_dir() . '/http_cache');
+ }
}
diff --git a/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php b/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php
index 745e074157974..5d484edacc1b7 100644
--- a/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php
+++ b/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php
@@ -160,7 +160,7 @@ protected function generateDataForMeta(BundleEntryReaderInterface $reader, strin
$alpha3ToAlpha2 = array_flip($alpha2ToAlpha3);
asort($alpha3ToAlpha2);
- $alpha2ToNumeric = $this->generateAlpha2ToNumericMapping($metadataBundle);
+ $alpha2ToNumeric = $this->generateAlpha2ToNumericMapping(array_flip($this->regionCodes), $metadataBundle);
$numericToAlpha2 = [];
foreach ($alpha2ToNumeric as $alpha2 => $numeric) {
// Add underscore prefix to force keys with leading zeros to remain as string keys.
@@ -231,7 +231,7 @@ private function generateAlpha2ToAlpha3Mapping(array $countries, ArrayAccessible
return $alpha2ToAlpha3;
}
- private function generateAlpha2ToNumericMapping(ArrayAccessibleResourceBundle $metadataBundle): array
+ private function generateAlpha2ToNumericMapping(array $countries, ArrayAccessibleResourceBundle $metadataBundle): array
{
$aliases = iterator_to_array($metadataBundle['alias']['territory']);
@@ -250,6 +250,10 @@ private function generateAlpha2ToNumericMapping(ArrayAccessibleResourceBundle $m
continue;
}
+ if (!isset($countries[$data['replacement']])) {
+ continue;
+ }
+
if ('deprecated' === $data['reason']) {
continue;
}
diff --git a/src/Symfony/Component/Intl/Resources/data/currencies/en_NO.php b/src/Symfony/Component/Intl/Resources/data/currencies/en_NO.php
new file mode 100644
index 0000000000000..dc28340678e53
--- /dev/null
+++ b/src/Symfony/Component/Intl/Resources/data/currencies/en_NO.php
@@ -0,0 +1,10 @@
+ [
+ 'NOK' => [
+ 'kr',
+ 'Norwegian Krone',
+ ],
+ ],
+];
diff --git a/src/Symfony/Component/Intl/Resources/data/regions/meta.php b/src/Symfony/Component/Intl/Resources/data/regions/meta.php
index 1c9f233273af7..e0a99ccb7f5a8 100644
--- a/src/Symfony/Component/Intl/Resources/data/regions/meta.php
+++ b/src/Symfony/Component/Intl/Resources/data/regions/meta.php
@@ -755,7 +755,6 @@
'ZWE' => 'ZW',
],
'Alpha2ToNumeric' => [
- 'AA' => '958',
'AD' => '020',
'AE' => '784',
'AF' => '004',
@@ -943,18 +942,6 @@
'PW' => '585',
'PY' => '600',
'QA' => '634',
- 'QM' => '959',
- 'QN' => '960',
- 'QP' => '962',
- 'QQ' => '963',
- 'QR' => '964',
- 'QS' => '965',
- 'QT' => '966',
- 'QV' => '968',
- 'QW' => '969',
- 'QX' => '970',
- 'QY' => '971',
- 'QZ' => '972',
'RE' => '638',
'RO' => '642',
'RS' => '688',
@@ -1012,29 +999,6 @@
'VU' => '548',
'WF' => '876',
'WS' => '882',
- 'XC' => '975',
- 'XD' => '976',
- 'XE' => '977',
- 'XF' => '978',
- 'XG' => '979',
- 'XH' => '980',
- 'XI' => '981',
- 'XJ' => '982',
- 'XL' => '984',
- 'XM' => '985',
- 'XN' => '986',
- 'XO' => '987',
- 'XP' => '988',
- 'XQ' => '989',
- 'XR' => '990',
- 'XS' => '991',
- 'XT' => '992',
- 'XU' => '993',
- 'XV' => '994',
- 'XW' => '995',
- 'XX' => '996',
- 'XY' => '997',
- 'XZ' => '998',
'YE' => '887',
'YT' => '175',
'ZA' => '710',
@@ -1042,7 +1006,6 @@
'ZW' => '716',
],
'NumericToAlpha2' => [
- '_958' => 'AA',
'_020' => 'AD',
'_784' => 'AE',
'_004' => 'AF',
@@ -1230,18 +1193,6 @@
'_585' => 'PW',
'_600' => 'PY',
'_634' => 'QA',
- '_959' => 'QM',
- '_960' => 'QN',
- '_962' => 'QP',
- '_963' => 'QQ',
- '_964' => 'QR',
- '_965' => 'QS',
- '_966' => 'QT',
- '_968' => 'QV',
- '_969' => 'QW',
- '_970' => 'QX',
- '_971' => 'QY',
- '_972' => 'QZ',
'_638' => 'RE',
'_642' => 'RO',
'_688' => 'RS',
@@ -1299,29 +1250,6 @@
'_548' => 'VU',
'_876' => 'WF',
'_882' => 'WS',
- '_975' => 'XC',
- '_976' => 'XD',
- '_977' => 'XE',
- '_978' => 'XF',
- '_979' => 'XG',
- '_980' => 'XH',
- '_981' => 'XI',
- '_982' => 'XJ',
- '_984' => 'XL',
- '_985' => 'XM',
- '_986' => 'XN',
- '_987' => 'XO',
- '_988' => 'XP',
- '_989' => 'XQ',
- '_990' => 'XR',
- '_991' => 'XS',
- '_992' => 'XT',
- '_993' => 'XU',
- '_994' => 'XV',
- '_995' => 'XW',
- '_996' => 'XX',
- '_997' => 'XY',
- '_998' => 'XZ',
'_887' => 'YE',
'_175' => 'YT',
'_710' => 'ZA',
diff --git a/src/Symfony/Component/Intl/Tests/CountriesTest.php b/src/Symfony/Component/Intl/Tests/CountriesTest.php
index 7b921036b2a00..01f0f76f2e40a 100644
--- a/src/Symfony/Component/Intl/Tests/CountriesTest.php
+++ b/src/Symfony/Component/Intl/Tests/CountriesTest.php
@@ -527,7 +527,6 @@ class CountriesTest extends ResourceBundleTestCase
];
private const ALPHA2_TO_NUMERIC = [
- 'AA' => '958',
'AD' => '020',
'AE' => '784',
'AF' => '004',
@@ -715,18 +714,6 @@ class CountriesTest extends ResourceBundleTestCase
'PW' => '585',
'PY' => '600',
'QA' => '634',
- 'QM' => '959',
- 'QN' => '960',
- 'QP' => '962',
- 'QQ' => '963',
- 'QR' => '964',
- 'QS' => '965',
- 'QT' => '966',
- 'QV' => '968',
- 'QW' => '969',
- 'QX' => '970',
- 'QY' => '971',
- 'QZ' => '972',
'RE' => '638',
'RO' => '642',
'RS' => '688',
@@ -784,29 +771,6 @@ class CountriesTest extends ResourceBundleTestCase
'VU' => '548',
'WF' => '876',
'WS' => '882',
- 'XC' => '975',
- 'XD' => '976',
- 'XE' => '977',
- 'XF' => '978',
- 'XG' => '979',
- 'XH' => '980',
- 'XI' => '981',
- 'XJ' => '982',
- 'XL' => '984',
- 'XM' => '985',
- 'XN' => '986',
- 'XO' => '987',
- 'XP' => '988',
- 'XQ' => '989',
- 'XR' => '990',
- 'XS' => '991',
- 'XT' => '992',
- 'XU' => '993',
- 'XV' => '994',
- 'XW' => '995',
- 'XX' => '996',
- 'XY' => '997',
- 'XZ' => '998',
'YE' => '887',
'YT' => '175',
'ZA' => '710',
@@ -814,6 +778,19 @@ class CountriesTest extends ResourceBundleTestCase
'ZW' => '716',
];
+ public function testAllGettersGenerateTheSameDataSetCount()
+ {
+ $alpha2Count = count(Countries::getCountryCodes());
+ $alpha3Count = count(Countries::getAlpha3Codes());
+ $numericCodesCount = count(Countries::getNumericCodes());
+ $namesCount = count(Countries::getNames());
+
+ // we base all on Name count since it is the first to be generated
+ $this->assertEquals($namesCount, $alpha2Count, 'Alpha 2 count does not match');
+ $this->assertEquals($namesCount, $alpha3Count, 'Alpha 3 count does not match');
+ $this->assertEquals($namesCount, $numericCodesCount, 'Numeric codes count does not match');
+ }
+
public function testGetCountryCodes()
{
$this->assertSame(self::COUNTRIES, Countries::getCountryCodes());
@@ -992,7 +969,7 @@ public function testGetNumericCode()
public function testNumericCodeExists()
{
$this->assertTrue(Countries::numericCodeExists('250'));
- $this->assertTrue(Countries::numericCodeExists('982'));
+ $this->assertTrue(Countries::numericCodeExists('008'));
$this->assertTrue(Countries::numericCodeExists('716'));
$this->assertTrue(Countries::numericCodeExists('036'));
$this->assertFalse(Countries::numericCodeExists('667'));
diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php
index 84e2553a627cc..e5bfb4daddc2e 100644
--- a/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php
+++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php
@@ -22,7 +22,7 @@ final class MailerSendSmtpTransport extends EsmtpTransport
{
public function __construct(string $username, #[\SensitiveParameter] string $password, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
{
- parent::__construct('smtp.mailersend.net', 587, true, $dispatcher, $logger);
+ parent::__construct('smtp.mailersend.net', 587, false, $dispatcher, $logger);
$this->setUsername($username);
$this->setPassword($password);
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php
index fa34f9abb7caf..08879782a0bc3 100644
--- a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php
+++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php
@@ -98,9 +98,6 @@ public function testCustomHeader()
$this->assertEquals('amp-html-value', $payload['amp-html']);
}
- /**
- * @legacy
- */
public function testPrefixHeaderWithH()
{
$email = new Email();
diff --git a/src/Symfony/Component/Mailer/Command/MailerTestCommand.php b/src/Symfony/Component/Mailer/Command/MailerTestCommand.php
index 6cde762f5ed8c..8e00f629877c7 100644
--- a/src/Symfony/Component/Mailer/Command/MailerTestCommand.php
+++ b/src/Symfony/Component/Mailer/Command/MailerTestCommand.php
@@ -35,10 +35,10 @@ protected function configure(): void
{
$this
->addArgument('to', InputArgument::REQUIRED, 'The recipient of the message')
- ->addOption('from', null, InputOption::VALUE_OPTIONAL, 'The sender of the message', 'from@example.org')
- ->addOption('subject', null, InputOption::VALUE_OPTIONAL, 'The subject of the message', 'Testing transport')
- ->addOption('body', null, InputOption::VALUE_OPTIONAL, 'The body of the message', 'Testing body')
- ->addOption('transport', null, InputOption::VALUE_OPTIONAL, 'The transport to be used')
+ ->addOption('from', null, InputOption::VALUE_REQUIRED, 'The sender of the message', 'from@example.org')
+ ->addOption('subject', null, InputOption::VALUE_REQUIRED, 'The subject of the message', 'Testing transport')
+ ->addOption('body', null, InputOption::VALUE_REQUIRED, 'The body of the message', 'Testing body')
+ ->addOption('transport', null, InputOption::VALUE_REQUIRED, 'The transport to be used')
->setHelp(<<<'EOF'
The %command.name% command tests a Mailer transport by sending a simple email message:
diff --git a/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php
index a893441b03a9a..5de88e71fa247 100644
--- a/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php
+++ b/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php
@@ -16,6 +16,8 @@
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Transport\RoundRobinTransport;
use Symfony\Component\Mailer\Transport\TransportInterface;
+use Symfony\Component\Mime\Header\Headers;
+use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
/**
@@ -144,6 +146,27 @@ public function testSendOneDeadAndRecoveryWithinRetryPeriod()
$this->assertTransports($t, 1, []);
}
+ public function testSendOneDeadMessageAlterationsDoNotPersist()
+ {
+ $t1 = $this->createMock(TransportInterface::class);
+ $t1->expects($this->once())->method('send')
+ ->willReturnCallback(function (Message $message) {
+ $message->getHeaders()->addTextHeader('X-Transport-1', 'value');
+ throw new TransportException();
+ });
+ $t2 = $this->createMock(TransportInterface::class);
+ $t2->expects($this->once())->method('send');
+ $t = new RoundRobinTransport([$t1, $t2]);
+ $p = new \ReflectionProperty($t, 'cursor');
+ $p->setValue($t, 0);
+ $headers = new Headers();
+ $headers->addTextHeader('X-Shared', 'value');
+ $message = new Message($headers);
+ $t->send($message);
+ $this->assertSame($message->getHeaders()->get('X-Shared')->getBody(), 'value');
+ $this->assertFalse($message->getHeaders()->has('X-Transport-1'));
+ }
+
public function testFailureDebugInformation()
{
$t1 = $this->createMock(TransportInterface::class);
diff --git a/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php b/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php
index 4925b40d0bb6a..e48644f790b56 100644
--- a/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php
+++ b/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php
@@ -50,7 +50,7 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess
while ($transport = $this->getNextTransport()) {
try {
- return $transport->send($message, $envelope);
+ return $transport->send(clone $message, $envelope);
} catch (TransportExceptionInterface $e) {
$exception ??= new TransportException('All transports failed.');
$exception->appendDebug(\sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug()));
diff --git a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php
index 1a84381008318..9fcb6a5e17e80 100644
--- a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php
+++ b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php
@@ -313,7 +313,7 @@ private function convertToBytes(string $memoryLimit): int
} elseif (str_starts_with($max, '0')) {
$max = \intval($max, 8);
} else {
- $max = (int) $max;
+ $max = (float) $max;
}
switch (substr(rtrim($memoryLimit, 'b'), -1)) {
@@ -326,6 +326,6 @@ private function convertToBytes(string $memoryLimit): int
case 'k': $max *= 1024;
}
- return $max;
+ return (int) $max;
}
}
diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php
index 4c8d44e9b72fb..648060a6773aa 100644
--- a/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php
+++ b/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php
@@ -35,7 +35,7 @@ protected function configure(): void
new InputArgument('id', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Specific message id(s) to remove'),
new InputOption('all', null, InputOption::VALUE_NONE, 'Remove all failed messages from the transport'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Force the operation without confirmation'),
- new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
+ new InputOption('transport', null, InputOption::VALUE_REQUIRED, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
new InputOption('show-messages', null, InputOption::VALUE_NONE, 'Display messages before removing it (if multiple ids are given)'),
])
->setHelp(<<<'EOF'
diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php
index 15dbe84a37da3..32f535703cebe 100644
--- a/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php
+++ b/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php
@@ -65,8 +65,8 @@ protected function configure(): void
->setDefinition([
new InputArgument('id', InputArgument::IS_ARRAY, 'Specific message id(s) to retry'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Force action without confirmation'),
- new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
- new InputOption('keepalive', null, InputOption::VALUE_OPTIONAL, 'Whether to use the transport\'s keepalive mechanism if implemented', self::DEFAULT_KEEPALIVE_INTERVAL),
+ new InputOption('transport', null, InputOption::VALUE_REQUIRED, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
+ new InputOption('keepalive', null, InputOption::VALUE_REQUIRED, 'Whether to use the transport\'s keepalive mechanism if implemented', self::DEFAULT_KEEPALIVE_INTERVAL),
])
->setHelp(<<<'EOF'
The %command.name% retries message in the failure transport.
diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
index f052f86bf92c2..927e6705a0e94 100644
--- a/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
+++ b/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
@@ -35,7 +35,7 @@ protected function configure(): void
->setDefinition([
new InputArgument('id', InputArgument::OPTIONAL, 'Specific message id to show'),
new InputOption('max', null, InputOption::VALUE_REQUIRED, 'Maximum number of messages to list', 50),
- new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
+ new InputOption('transport', null, InputOption::VALUE_REQUIRED, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
new InputOption('stats', null, InputOption::VALUE_NONE, 'Display the message count by class'),
new InputOption('class-filter', null, InputOption::VALUE_REQUIRED, 'Filter by a specific class name'),
])
diff --git a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php
index 7790e074ad609..7183f2e7c67d7 100644
--- a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php
@@ -12,6 +12,8 @@
namespace Symfony\Component\Messenger\Tests\Command;
use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LoggerTrait;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Tester\CommandCompletionTester;
@@ -205,6 +207,48 @@ public function testRunWithTimeLimit()
$this->assertStringContainsString('[OK] Consuming messages from transport "dummy-receiver"', $tester->getDisplay());
}
+ public function testRunWithMemoryLimit()
+ {
+ $envelope = new Envelope(new \stdClass(), [new BusNameStamp('dummy-bus')]);
+
+ $receiver = $this->createMock(ReceiverInterface::class);
+ $receiver->method('get')->willReturn([$envelope]);
+
+ $receiverLocator = new Container();
+ $receiverLocator->set('dummy-receiver', $receiver);
+
+ $bus = $this->createMock(MessageBusInterface::class);
+
+ $busLocator = new Container();
+ $busLocator->set('dummy-bus', $bus);
+
+ $logger = new class() implements LoggerInterface {
+ use LoggerTrait;
+
+ public array $logs = [];
+
+ public function log(...$args): void
+ {
+ $this->logs[] = $args;
+ }
+ };
+ $command = new ConsumeMessagesCommand(new RoutableMessageBus($busLocator), $receiverLocator, new EventDispatcher(), $logger);
+
+ $application = new Application();
+ $application->add($command);
+ $tester = new CommandTester($application->get('messenger:consume'));
+ $tester->execute([
+ 'receivers' => ['dummy-receiver'],
+ '--memory-limit' => '1.5M',
+ ]);
+
+ $this->assertSame(0, $tester->getStatusCode());
+ $this->assertStringContainsString('[OK] Consuming messages from transport "dummy-receiver"', $tester->getDisplay());
+ $this->assertStringContainsString('The worker will automatically exit once it has exceeded 1.5M of memory', $tester->getDisplay());
+
+ $this->assertSame(1572864, $logger->logs[1][2]['limit']);
+ }
+
public function testRunWithAllOption()
{
$envelope1 = new Envelope(new \stdClass(), [new BusNameStamp('dummy-bus')]);
diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php b/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
index 67af3ac9237a7..65f48bcd7ac19 100644
--- a/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
@@ -75,13 +75,13 @@ protected function doSend(MessageInterface $message): SentMessage
$options['from'] = $message->getFrom() ?: $this->from;
$options['source'] ??= $this->source;
$options['list_id'] ??= $this->listId;
- $options['from_email'] ?? $this->fromEmail;
+ $options['from_email'] ??= $this->fromEmail;
if (isset($options['from']) && !preg_match('/^[a-zA-Z0-9\s]{3,11}$/', $options['from']) && !preg_match('/^\+[1-9]\d{1,14}$/', $options['from'])) {
throw new InvalidArgumentException(\sprintf('The "From" number "%s" is not a valid phone number, shortcode, or alphanumeric sender ID.', $options['from']));
}
- if ($options['list_id'] ?? false) {
+ if (!$options['list_id']) {
$options['to'] = $message->getPhone();
}
diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php b/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
index e1f9fa37dcae0..532c5aceba3aa 100644
--- a/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
+++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
@@ -24,7 +24,7 @@
final class ClickSendTransportTest extends TransportTestCase
{
- public static function createTransport(?HttpClientInterface $client = null, string $from = 'test_from', string $source = 'test_source', int $listId = 99, string $fromEmail = 'foo@bar.com'): ClickSendTransport
+ public static function createTransport(?HttpClientInterface $client = null, ?string $from = 'test_from', ?string $source = 'test_source', ?int $listId = 99, ?string $fromEmail = 'foo@bar.com'): ClickSendTransport
{
return new ClickSendTransport('test_username', 'test_key', $from, $source, $listId, $fromEmail, $client ?? new MockHttpClient());
}
@@ -70,6 +70,10 @@ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValid(string $from
$body = json_decode($options['body'], true);
self::assertIsArray($body);
self::assertArrayHasKey('messages', $body);
+ $message = reset($body['messages']);
+ self::assertArrayHasKey('from_email', $message);
+ self::assertArrayHasKey('list_id', $message);
+ self::assertArrayNotHasKey('to', $message);
return $response;
});
@@ -77,6 +81,29 @@ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValid(string $from
$transport->send($message);
}
+ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValidWithoutOptionalParameters()
+ {
+ $message = new SmsMessage('+33612345678', 'Hello!');
+ $response = $this->createMock(ResponseInterface::class);
+ $response->expects(self::exactly(2))->method('getStatusCode')->willReturn(200);
+ $response->expects(self::once())->method('getContent')->willReturn('');
+ $client = new MockHttpClient(function (string $method, string $url, array $options) use ($response): ResponseInterface {
+ self::assertSame('POST', $method);
+ self::assertSame('https://rest.clicksend.com/v3/sms/send', $url);
+
+ $body = json_decode($options['body'], true);
+ self::assertIsArray($body);
+ self::assertArrayHasKey('messages', $body);
+ $message = reset($body['messages']);
+ self::assertArrayNotHasKey('list_id', $message);
+ self::assertArrayHasKey('to', $message);
+
+ return $response;
+ });
+ $transport = $this->createTransport($client, null, null, null, null);
+ $transport->send($message);
+ }
+
public static function toStringProvider(): iterable
{
yield ['clicksend://rest.clicksend.com?from=test_from&source=test_source&list_id=99&from_email=foo%40bar.com', self::createTransport()];
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php
index e652e879ed64f..e182e66fb848a 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\Notifier\Bridge\FakeSms;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
@@ -20,6 +19,7 @@
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\Transport\AbstractTransport;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php
index ca90a24a449d9..7a37386875816 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php
@@ -12,12 +12,12 @@
namespace Symfony\Component\Notifier\Bridge\FakeSms;
use Psr\Log\LoggerInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException;
use Symfony\Component\Notifier\Message\MessageInterface;
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\Transport\AbstractTransport;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
index 8685407861ed1..9a2c82d0dcf61 100644
--- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
@@ -109,11 +109,6 @@ public function getValue(object|array $objectOrArray, string|PropertyPathInterfa
return $propertyValues[\count($propertyValues) - 1][self::VALUE];
}
- /**
- * @template T of object|array
- * @param T $objectOrArray
- * @param-out ($objectOrArray is array ? array : T) $objectOrArray
- */
public function setValue(object|array &$objectOrArray, string|PropertyPathInterface $propertyPath, mixed $value): void
{
if (\is_object($objectOrArray) && (false === strpbrk((string) $propertyPath, '.[') || $objectOrArray instanceof \stdClass && property_exists($objectOrArray, $propertyPath))) {
diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
index 2e25e9e517db2..ccbaf8b3c4b49 100644
--- a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
+++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
@@ -39,6 +39,12 @@ interface PropertyAccessorInterface
*
* If neither is found, an exception is thrown.
*
+ * @template T of object|array
+ *
+ * @param T $objectOrArray
+ *
+ * @param-out ($objectOrArray is array ? array : T) $objectOrArray
+ *
* @throws Exception\InvalidArgumentException If the property path is invalid
* @throws Exception\AccessException If a property/index does not exist or is not public
* @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
index d7aaac1b226a7..95ef9a3568537 100644
--- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
+++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
@@ -927,7 +927,7 @@ public static function unionTypesProvider(): iterable
Type::object(ParentDummy::class),
Type::null(),
)];
- yield ['f', null];
+ yield ['f', Type::union(Type::string(), Type::null())];
yield ['g', Type::array(Type::union(Type::string(), Type::int()))];
}
diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json
index 65f2782ed909c..7a43c88254a07 100644
--- a/src/Symfony/Component/PropertyInfo/composer.json
+++ b/src/Symfony/Component/PropertyInfo/composer.json
@@ -25,7 +25,7 @@
"require": {
"php": ">=8.2",
"symfony/string": "^6.4|^7.0",
- "symfony/type-info": "~7.1.9|^7.2.2"
+ "symfony/type-info": "~7.2.8|^7.3.1"
},
"require-dev": {
"symfony/serializer": "^6.4|^7.0",
diff --git a/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php
index a252814570f2e..c0c290e686800 100644
--- a/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php
+++ b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php
@@ -20,7 +20,7 @@ class BasicErrorHandler
{
public static function register(bool $debug): void
{
- error_reporting(-1);
+ error_reporting(\E_ALL & ~\E_DEPRECATED & ~\E_USER_DEPRECATED);
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
ini_set('display_errors', $debug);
diff --git a/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php b/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php
index 0dfc7de0ca7a0..47c67605b0430 100644
--- a/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php
+++ b/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php
@@ -30,7 +30,7 @@ public static function register(bool $debug): void
return;
}
- error_reporting(-1);
+ error_reporting(\E_ALL & ~\E_DEPRECATED & ~\E_USER_DEPRECATED);
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
ini_set('display_errors', $debug);
diff --git a/src/Symfony/Component/Runtime/SymfonyRuntime.php b/src/Symfony/Component/Runtime/SymfonyRuntime.php
index c66035f9abaf0..f798a756d0914 100644
--- a/src/Symfony/Component/Runtime/SymfonyRuntime.php
+++ b/src/Symfony/Component/Runtime/SymfonyRuntime.php
@@ -150,7 +150,11 @@ public function getRunner(?object $application): RunnerInterface
if (!$application->getName() || !$console->has($application->getName())) {
$application->setName($_SERVER['argv'][0]);
- $console->add($application);
+ if (method_exists($console, 'addCommand')) {
+ $console->addCommand($application);
+ } else {
+ $console->add($application);
+ }
}
$console->setDefaultCommand($application->getName(), true);
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/application.php b/src/Symfony/Component/Runtime/Tests/phpt/application.php
index ca2de555edfb7..b51947c2afaf1 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/application.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/application.php
@@ -25,7 +25,11 @@
});
$app = new Application();
- $app->add($command);
+ if (method_exists($app, 'addCommand')) {
+ $app->addCommand($command);
+ } else {
+ $app->add($command);
+ }
$app->setDefaultCommand('go', true);
return $app;
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/command_list.php b/src/Symfony/Component/Runtime/Tests/phpt/command_list.php
index 929b4401e86b9..aa40eda627151 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/command_list.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/command_list.php
@@ -23,7 +23,11 @@
$command->setName('my_command');
[$cmd, $args] = $runtime->getResolver(require __DIR__.'/command.php')->resolve();
- $app->add($cmd(...$args));
+ if (method_exists($app, 'addCommand')) {
+ $app->addCommand($cmd(...$args));
+ } else {
+ $app->add($cmd(...$args));
+ }
return $app;
};
diff --git a/src/Symfony/Component/Security/Core/User/UserInterface.php b/src/Symfony/Component/Security/Core/User/UserInterface.php
index e6078399d685b..904b933d557af 100644
--- a/src/Symfony/Component/Security/Core/User/UserInterface.php
+++ b/src/Symfony/Component/Security/Core/User/UserInterface.php
@@ -15,9 +15,7 @@
* Represents the interface that all user classes must implement.
*
* This interface is useful because the authentication layer can deal with
- * the object through its lifecycle, using the object to get the hashed
- * password (for checking against a submitted password), assigning roles
- * and so on.
+ * the object through its lifecycle, assigning roles and so on.
*
* Regardless of how your users are loaded or where they come from (a database,
* configuration, web service, etc.), you will have a class that implements
diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php
index 6c256dba60955..da616e86ccc99 100644
--- a/src/Symfony/Component/Security/Http/Firewall.php
+++ b/src/Symfony/Component/Security/Http/Firewall.php
@@ -122,7 +122,11 @@ public static function getSubscribedEvents()
protected function callListeners(RequestEvent $event, iterable $listeners)
{
foreach ($listeners as $listener) {
- $listener($event);
+ if (!$listener instanceof FirewallListenerInterface) {
+ $listener($event);
+ } elseif (false !== $listener->supports($event->getRequest())) {
+ $listener->authenticate($event);
+ }
if ($event->hasResponse()) {
break;
diff --git a/src/Symfony/Component/Security/Http/FirewallMap.php b/src/Symfony/Component/Security/Http/FirewallMap.php
index 3b01cbdc161a6..444f71ceebbda 100644
--- a/src/Symfony/Component/Security/Http/FirewallMap.php
+++ b/src/Symfony/Component/Security/Http/FirewallMap.php
@@ -14,6 +14,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
@@ -25,12 +26,12 @@
class FirewallMap implements FirewallMapInterface
{
/**
- * @var list, ExceptionListener|null, LogoutListener|null}>
+ * @var list, ExceptionListener|null, LogoutListener|null}>
*/
private array $map = [];
/**
- * @param list $listeners
+ * @param list $listeners
*/
public function add(?RequestMatcherInterface $requestMatcher = null, array $listeners = [], ?ExceptionListener $exceptionListener = null, ?LogoutListener $logoutListener = null): void
{
diff --git a/src/Symfony/Component/Security/Http/FirewallMapInterface.php b/src/Symfony/Component/Security/Http/FirewallMapInterface.php
index fa43d6a6e9ba3..1925d3dec23a0 100644
--- a/src/Symfony/Component/Security/Http/FirewallMapInterface.php
+++ b/src/Symfony/Component/Security/Http/FirewallMapInterface.php
@@ -13,6 +13,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
@@ -35,7 +36,7 @@ interface FirewallMapInterface
* If there is no logout listener, the third element of the outer array
* must be null.
*
- * @return array{iterable, ExceptionListener, LogoutListener}
+ * @return array{iterable, ExceptionListener, LogoutListener}
*/
public function getListeners(Request $request): array;
}
diff --git a/src/Symfony/Component/Security/Http/Tests/FirewallTest.php b/src/Symfony/Component/Security/Http/Tests/FirewallTest.php
index f9417d237433c..89040f3875f2b 100644
--- a/src/Symfony/Component/Security/Http/Tests/FirewallTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/FirewallTest.php
@@ -18,7 +18,9 @@
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Http\Firewall;
+use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\FirewallMapInterface;
class FirewallTest extends TestCase
@@ -97,4 +99,59 @@ public function testOnKernelRequestWithSubRequest()
$this->assertFalse($event->hasResponse());
}
+
+ public function testListenersAreCalled()
+ {
+ $calledListeners = [];
+
+ $callableListener = static function() use(&$calledListeners) { $calledListeners[] = 'callableListener'; };
+ $firewallListener = new class($calledListeners) implements FirewallListenerInterface {
+ public function __construct(private array &$calledListeners) {}
+
+ public function supports(Request $request): ?bool
+ {
+ return true;
+ }
+
+ public function authenticate(RequestEvent $event): void
+ {
+ $this->calledListeners[] = 'firewallListener';
+ }
+
+ public static function getPriority(): int
+ {
+ return 0;
+ }
+ };
+ $callableFirewallListener = new class($calledListeners) extends AbstractListener {
+ public function __construct(private array &$calledListeners) {}
+
+ public function supports(Request $request): ?bool
+ {
+ return true;
+ }
+
+ public function authenticate(RequestEvent $event): void
+ {
+ $this->calledListeners[] = 'callableFirewallListener';
+ }
+ };
+
+ $request = $this->createMock(Request::class);
+
+ $map = $this->createMock(FirewallMapInterface::class);
+ $map
+ ->expects($this->once())
+ ->method('getListeners')
+ ->with($this->equalTo($request))
+ ->willReturn([[$callableListener, $firewallListener, $callableFirewallListener], null, null])
+ ;
+
+ $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
+
+ $firewall = new Firewall($map, $this->createMock(EventDispatcherInterface::class));
+ $firewall->onKernelRequest($event);
+
+ $this->assertSame(['callableListener', 'firewallListener', 'callableFirewallListener'], $calledListeners);
+ }
}
diff --git a/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php b/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
index a05bf4bf8655c..b76123fc3e9a9 100644
--- a/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
+++ b/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
@@ -171,8 +171,8 @@ private function getCaller(string $method, string $interface): array
&& $method === $trace[$i]['function']
&& is_a($trace[$i]['class'], $interface, true)
) {
- $file = $trace[$i]['file'];
- $line = $trace[$i]['line'];
+ $file = $trace[$i]['file'] ?? $trace[$i + 1]['file'];
+ $line = $trace[$i]['line'] ?? $trace[$i + 1]['line'];
break;
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
index 00d2e3b00ef83..06086e83ad168 100644
--- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
@@ -26,6 +26,7 @@
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
+use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
@@ -1069,6 +1070,30 @@ protected function createChildContext(array $parentContext, string $attribute, ?
return $context;
}
+ protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
+ {
+ if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) {
+ return false;
+ }
+
+ if (null !== $this->classDiscriminatorResolver) {
+ $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject;
+ if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) {
+ $allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty());
+ }
+
+ if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
+ $attributes = [];
+ foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
+ $attributes[] = parent::getAllowedAttributes($mappedClass, $context, $attributesAsString);
+ }
+ $allowedAttributes = array_merge($allowedAttributes, ...$attributes);
+ }
+ }
+
+ return $allowedAttributes;
+ }
+
/**
* Builds the cache key for the attributes cache.
*
diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
index 1d60cba50b0c3..cbba35ba0f674 100644
--- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
@@ -20,7 +20,6 @@
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
use Symfony\Component\Serializer\Annotation\Ignore;
use Symfony\Component\Serializer\Exception\LogicException;
-use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -149,30 +148,6 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v
}
}
- protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
- {
- if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) {
- return false;
- }
-
- if (null !== $this->classDiscriminatorResolver) {
- $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject;
- if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) {
- $allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty());
- }
-
- if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
- $attributes = [];
- foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
- $attributes[] = parent::getAllowedAttributes($mappedClass, $context, $attributesAsString);
- }
- $allowedAttributes = array_merge($allowedAttributes, ...$attributes);
- }
- }
-
- return $allowedAttributes;
- }
-
protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []): bool
{
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php
index f95f2d72e0b46..7308bfc7c754a 100644
--- a/src/Symfony/Component/Serializer/Serializer.php
+++ b/src/Symfony/Component/Serializer/Serializer.php
@@ -213,7 +213,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
throw new NotNormalizableValueException(\sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type));
}
- if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) {
+ if ((isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) && !isset($context['not_normalizable_value_exceptions'])) {
unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]);
$context['not_normalizable_value_exceptions'] = [];
$errors = &$context['not_normalizable_value_exceptions'];
diff --git a/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php b/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
index d697b270ff958..886fb12e97a8f 100644
--- a/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
@@ -128,6 +128,40 @@ public function testAddDebugTraceIdInContext()
$traceableSerializer->encode('data', 'format');
$traceableSerializer->decode('data', 'format');
}
+
+ public function testCollectedCaller()
+ {
+ $serializer = new \Symfony\Component\Serializer\Serializer();
+
+ $collector = new SerializerDataCollector();
+ $traceableSerializer = new TraceableSerializer($serializer, $collector);
+
+ $traceableSerializer->normalize('data');
+ $collector->lateCollect();
+
+ $this->assertSame([
+ 'name' => 'TraceableSerializerTest.php',
+ 'file' => __FILE__,
+ 'line' => __LINE__ - 6,
+ ], $collector->getData()['normalize'][0]['caller']);
+ }
+
+ public function testCollectedCallerFromArrayMap()
+ {
+ $serializer = new \Symfony\Component\Serializer\Serializer();
+
+ $collector = new SerializerDataCollector();
+ $traceableSerializer = new TraceableSerializer($serializer, $collector);
+
+ array_map([$traceableSerializer, 'normalize'], ['data']);
+ $collector->lateCollect();
+
+ $this->assertSame([
+ 'name' => 'TraceableSerializerTest.php',
+ 'file' => __FILE__,
+ 'line' => __LINE__ - 6,
+ ], $collector->getData()['normalize'][0]['caller']);
+ }
}
class Serializer implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php
index 31206ea67d289..ea26589a2b072 100644
--- a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php
@@ -20,6 +20,7 @@
'one' => DummyMessageNumberOne::class,
'two' => DummyMessageNumberTwo::class,
'three' => DummyMessageNumberThree::class,
+ 'four' => DummyMessageNumberFour::class,
])]
interface DummyMessageInterface
{
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php
new file mode 100644
index 0000000000000..eaf87d48a7101
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.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\Serializer\Tests\Fixtures;
+
+use Symfony\Component\Serializer\Attribute\Ignore;
+
+abstract class SomeAbstract {
+ #[Ignore]
+ public function getDescription()
+ {
+ return 'Hello, World!';
+ }
+}
+
+class DummyMessageNumberFour extends SomeAbstract implements DummyMessageInterface
+{
+ public function __construct(public $one)
+ {
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php
index 270b65f33ef66..2afa412786b92 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php
@@ -45,6 +45,7 @@
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
+use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
@@ -52,6 +53,8 @@
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild;
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild;
use Symfony\Component\Serializer\Tests\Fixtures\DummyFirstChildQuux;
+use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface;
+use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberFour;
use Symfony\Component\Serializer\Tests\Fixtures\DummySecondChildQuux;
use Symfony\Component\Serializer\Tests\Fixtures\DummyString;
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithNotNormalizable;
@@ -1200,6 +1203,25 @@ public static function provideBooleanTypesData()
];
}
+ public function testDeserializeAndSerializeConstructorAndIgnoreAndInterfacedObjectsWithTheClassMetadataDiscriminator()
+ {
+ $example = new DummyMessageNumberFour('Hello');
+
+ $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
+
+ $normalizer = new PropertyNormalizer(
+ $classMetadataFactory,
+ null,
+ new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]),
+ new ClassDiscriminatorFromClassMetadata($classMetadataFactory),
+ );
+
+ $serialized = $normalizer->normalize($example, 'json');
+ $deserialized = $normalizer->denormalize($serialized, DummyMessageInterface::class, 'json');
+
+ $this->assertEquals($example, $deserialized);
+ }
+
/**
* @dataProvider provideDenormalizeWithFilterBoolData
*/
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
index d45586b4444ee..83b0bbcdbbbf0 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
@@ -748,6 +748,8 @@ public function testDoesntHaveIssuesWithUnionConstTypes()
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]);
$this->assertSame('bar', $serializer->denormalize(['foo' => 'bar'], (new class {
+ public const TEST = 'me';
+
/** @var self::*|null */
public $foo;
})::class)->foo);
diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php
index 8fd0ff8850f63..7cfa8becfa245 100644
--- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php
@@ -1700,6 +1700,54 @@ public function testCollectDenormalizationErrorsDefaultContext()
$serializer->denormalize($data, DummyWithVariadicParameter::class);
}
+
+ public function testDenormalizationFailsWithMultipleErrorsInDefaultContext()
+ {
+ $serializer = new Serializer(
+ [new DateTimeNormalizer(), new ObjectNormalizer()],
+ [],
+ [DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true]
+ );
+
+ $data = ['date' => '', 'unknown' => null];
+
+ try {
+ $serializer->denormalize($data, DummyEntityWithStringAndDateTime::class);
+ $this->fail('Expected PartialDenormalizationException was not thrown');
+ } catch (PartialDenormalizationException $e) {
+ $this->assertIsArray($e->getErrors());
+ $this->assertCount(2, $e->getErrors(), 'Expected two denormalization errors');
+
+ $exceptionsAsArray = array_map(function (NotNormalizableValueException $ex): array {
+ return [
+ 'currentType' => $ex->getCurrentType(),
+ 'expectedTypes' => $ex->getExpectedTypes(),
+ 'path' => $ex->getPath(),
+ 'useMessageForUser' => $ex->canUseMessageForUser(),
+ 'message' => $ex->getMessage(),
+ ];
+ }, $e->getErrors());
+
+ $expected = [
+ [
+ 'currentType' => 'null',
+ 'expectedTypes' => ['string'],
+ 'path' => 'bar',
+ 'useMessageForUser' => true,
+ 'message' => 'Failed to create object because the class misses the "bar" property.',
+ ],
+ [
+ 'currentType' => 'string',
+ 'expectedTypes' => ['string'],
+ 'path' => 'date',
+ 'useMessageForUser' => true,
+ 'message' => 'The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.',
+ ],
+ ];
+
+ $this->assertSame($expected, $exceptionsAsArray);
+ }
+ }
}
class Model
@@ -1766,6 +1814,15 @@ public function __construct($value)
}
}
+class DummyEntityWithStringAndDateTime
+{
+ public function __construct(
+ public string $bar,
+ public \DateTimeInterface $date,
+ ) {
+ }
+}
+
class DummyUnionType
{
/**
diff --git a/src/Symfony/Component/Translation/Command/TranslationPullCommand.php b/src/Symfony/Component/Translation/Command/TranslationPullCommand.php
index ad42716f55583..3324bd56e2e35 100644
--- a/src/Symfony/Component/Translation/Command/TranslationPullCommand.php
+++ b/src/Symfony/Component/Translation/Command/TranslationPullCommand.php
@@ -84,10 +84,10 @@ protected function configure(): void
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider),
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'),
new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'),
- new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
- new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
- new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'),
- new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'),
+ new InputOption('domains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
+ new InputOption('locales', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
+ new InputOption('format', null, InputOption::VALUE_REQUIRED, 'Override the default output format.', 'xlf12'),
+ new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'),
])
->setHelp(<<<'EOF'
The %command.name%> command pulls translations from the given provider. Only
diff --git a/src/Symfony/Component/Translation/Command/TranslationPushCommand.php b/src/Symfony/Component/Translation/Command/TranslationPushCommand.php
index b1cdc5fc0b87c..7208ca25fe8de 100644
--- a/src/Symfony/Component/Translation/Command/TranslationPushCommand.php
+++ b/src/Symfony/Component/Translation/Command/TranslationPushCommand.php
@@ -77,8 +77,8 @@ protected function configure(): void
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider),
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'),
new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'),
- new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
- new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
+ new InputOption('domains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
+ new InputOption('locales', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
])
->setHelp(<<<'EOF'
The %command.name%> command pushes translations to the given provider. Only new
diff --git a/src/Symfony/Component/TypeInfo/.gitattributes b/src/Symfony/Component/TypeInfo/.gitattributes
index 14c3c35940427..413aef4cac05d 100644
--- a/src/Symfony/Component/TypeInfo/.gitattributes
+++ b/src/Symfony/Component/TypeInfo/.gitattributes
@@ -1,3 +1,4 @@
/Tests export-ignore
+/Tests/Fixtures/DummyWithUsesWindowsLineEndings.php text eol=crlf
/phpunit.xml.dist export-ignore
/.git* export-ignore
diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithConstants.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithConstants.php
new file mode 100644
index 0000000000000..c849fd59b504d
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithConstants.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\Fixtures;
+
+final class DummyWithConstants
+{
+ public const DUMMY_STRING_A = 'a';
+ public const DUMMY_INT_A = 1;
+ public const DUMMY_FLOAT_A = 1.23;
+ public const DUMMY_TRUE_A = true;
+ public const DUMMY_FALSE_A = false;
+ public const DUMMY_NULL_A = null;
+ public const DUMMY_ARRAY_A = [];
+ public const DUMMY_ENUM_A = DummyEnum::ONE;
+
+ public const DUMMY_MIX_1 = self::DUMMY_STRING_A;
+ public const DUMMY_MIX_2 = self::DUMMY_INT_A;
+ public const DUMMY_MIX_3 = self::DUMMY_FLOAT_A;
+ public const DUMMY_MIX_4 = self::DUMMY_TRUE_A;
+ public const DUMMY_MIX_5 = self::DUMMY_FALSE_A;
+ public const DUMMY_MIX_6 = self::DUMMY_NULL_A;
+ public const DUMMY_MIX_7 = self::DUMMY_ARRAY_A;
+ public const DUMMY_MIX_8 = self::DUMMY_ENUM_A;
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUsesWindowsLineEndings.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUsesWindowsLineEndings.php
new file mode 100644
index 0000000000000..9c7d09be76370
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUsesWindowsLineEndings.php
@@ -0,0 +1,22 @@
+createdAt = $createdAt;
+ }
+
+ public function getType(): Type
+ {
+ throw new \LogicException('Should not be called.');
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
index 1de82676b0334..c277449659d10 100644
--- a/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
@@ -16,6 +16,7 @@
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUses;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUsesWindowsLineEndings;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
@@ -82,6 +83,24 @@ public function testCollectUses()
$this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionParameter([DummyWithUses::class, 'setCreatedAt'], 'createdAt'))->uses);
}
+ public function testCollectUsesWindowsLineEndings()
+ {
+ self::assertSame(\count(file(__DIR__.'/../Fixtures/DummyWithUsesWindowsLineEndings.php')), substr_count(file_get_contents(__DIR__.'/../Fixtures/DummyWithUsesWindowsLineEndings.php'), "\r\n"));
+
+ $uses = [
+ 'Type' => Type::class,
+ \DateTimeInterface::class => '\\'.\DateTimeInterface::class,
+ 'DateTime' => '\\'.\DateTimeImmutable::class,
+ ];
+
+ $this->assertSame($uses, $this->typeContextFactory->createFromClassName(DummyWithUsesWindowsLineEndings::class)->uses);
+
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithUsesWindowsLineEndings::class))->uses);
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithUsesWindowsLineEndings::class, 'createdAt'))->uses);
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithUsesWindowsLineEndings::class, 'setCreatedAt'))->uses);
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionParameter([DummyWithUsesWindowsLineEndings::class, 'setCreatedAt'], 'createdAt'))->uses);
+ }
+
public function testCollectTemplates()
{
$this->assertEquals([], $this->typeContextFactory->createFromClassName(Dummy::class)->templates);
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
index 9320987c6baed..878699e5fdb12 100644
--- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
@@ -19,6 +19,7 @@
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyCollection;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithConstants;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeContext\TypeContext;
@@ -90,6 +91,19 @@ public static function resolveDataProvider(): iterable
yield [Type::string(), '"string"'];
yield [Type::true(), 'true'];
+ // const fetch
+ yield [Type::string(), DummyWithConstants::class.'::DUMMY_STRING_*'];
+ yield [Type::string(), DummyWithConstants::class.'::DUMMY_STRING_A'];
+ yield [Type::int(), DummyWithConstants::class.'::DUMMY_INT_*'];
+ yield [Type::int(), DummyWithConstants::class.'::DUMMY_INT_A'];
+ yield [Type::float(), DummyWithConstants::class.'::DUMMY_FLOAT_*'];
+ yield [Type::bool(), DummyWithConstants::class.'::DUMMY_TRUE_*'];
+ yield [Type::bool(), DummyWithConstants::class.'::DUMMY_FALSE_*'];
+ yield [Type::null(), DummyWithConstants::class.'::DUMMY_NULL_*'];
+ yield [Type::array(), DummyWithConstants::class.'::DUMMY_ARRAY_*'];
+ yield [Type::enum(DummyEnum::class, Type::string()), DummyWithConstants::class.'::DUMMY_ENUM_*'];
+ yield [Type::union(Type::string(), Type::int(), Type::float(), Type::bool(), Type::null(), Type::array(), Type::enum(DummyEnum::class, Type::string())), DummyWithConstants::class.'::DUMMY_MIX_*'];
+
// identifiers
yield [Type::bool(), 'bool'];
yield [Type::bool(), 'boolean'];
@@ -152,6 +166,9 @@ public static function resolveDataProvider(): iterable
yield [Type::generic(Type::object(\DateTime::class), Type::string(), Type::bool()), \DateTime::class.''];
yield [Type::generic(Type::object(\DateTime::class), Type::generic(Type::object(\Stringable::class), Type::bool())), \sprintf('%s<%s>', \DateTime::class, \Stringable::class)];
yield [Type::int(), 'int<0, 100>'];
+ yield [Type::string(), \sprintf('value-of<%s>', DummyBackedEnum::class)];
+ yield [Type::int(), 'key-of>'];
+ yield [Type::bool(), 'value-of>'];
// union
yield [Type::union(Type::int(), Type::string()), 'int|string'];
@@ -207,9 +224,21 @@ public function testCannotResolveParentWithoutTypeContext()
$this->resolver->resolve('parent');
}
- public function testCannotUnknownIdentifier()
+ public function testCannotResolveUnknownIdentifier()
{
$this->expectException(UnsupportedException::class);
$this->resolver->resolve('unknown');
}
+
+ public function testCannotResolveKeyOfInvalidType()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve('key-of');
+ }
+
+ public function testCannotResolveValueOfInvalidType()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve('value-of');
+ }
}
diff --git a/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
index 8cf405bd76696..5d08c309622d1 100644
--- a/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
+++ b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
@@ -116,7 +116,7 @@ private function collectUses(\ReflectionClass $reflection): array
return [];
}
- if (false === $lines = @file($fileName)) {
+ if (false === $lines = @file($fileName, \FILE_IGNORE_NEW_LINES)) {
throw new RuntimeException(\sprintf('Unable to read file "%s".', $fileName));
}
@@ -126,7 +126,7 @@ private function collectUses(\ReflectionClass $reflection): array
foreach ($lines as $line) {
if (str_starts_with($line, 'use ')) {
$inUseSection = true;
- $use = explode(' as ', substr($line, 4, -2), 2);
+ $use = explode(' as ', substr($line, 4, -1), 2);
$alias = 1 === \count($use) ? substr($use[0], false !== ($p = strrpos($use[0], '\\')) ? 1 + $p : 0) : $use[1];
$uses[$alias] = $use[0];
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
index a172d388a8722..1ea4a05eed583 100644
--- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
@@ -18,6 +18,7 @@
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode;
+use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
@@ -38,6 +39,7 @@
use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BackedEnumType;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\GenericType;
@@ -118,6 +120,47 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ
}
if ($node instanceof ConstTypeNode) {
+ if ($node->constExpr instanceof ConstFetchNode) {
+ $className = match (strtolower($node->constExpr->className)) {
+ 'self' => $typeContext->getDeclaringClass(),
+ 'static' => $typeContext->getCalledClass(),
+ 'parent' => $typeContext->getParentClass(),
+ default => $node->constExpr->className,
+ };
+
+ if (!class_exists($className)) {
+ return Type::mixed();
+ }
+
+ $types = [];
+
+ foreach ((new \ReflectionClass($className))->getReflectionConstants() as $const) {
+ if (preg_match('/^'.str_replace('\*', '.*', preg_quote($node->constExpr->name, '/')).'$/', $const->getName())) {
+ $constValue = $const->getValue();
+
+ $types[] = match (true) {
+ true === $constValue,
+ false === $constValue => Type::bool(),
+ null === $constValue => Type::null(),
+ \is_string($constValue) => Type::string(),
+ \is_int($constValue) => Type::int(),
+ \is_float($constValue) => Type::float(),
+ \is_array($constValue) => Type::array(),
+ $constValue instanceof \UnitEnum => Type::enum($constValue::class),
+ default => Type::mixed(),
+ };
+ }
+ }
+
+ $types = array_unique($types);
+
+ if (\count($types) > 2) {
+ return Type::union(...$types);
+ }
+
+ return $types[0] ?? Type::null();
+ }
+
return match ($node->constExpr::class) {
ConstExprArrayNode::class => Type::array(),
ConstExprFalseNode::class => Type::false(),
@@ -182,6 +225,28 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ
}
if ($node instanceof GenericTypeNode) {
+ if ($node->type instanceof IdentifierTypeNode && 'value-of' === $node->type->name) {
+ $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext);
+ if ($type instanceof BackedEnumType) {
+ return $type->getBackingType();
+ }
+
+ if ($type instanceof CollectionType) {
+ return $type->getCollectionValueType();
+ }
+
+ throw new \DomainException(\sprintf('"%s" is not a valid type for "value-of".', $node->genericTypes[0]));
+ }
+
+ if ($node->type instanceof IdentifierTypeNode && 'key-of' === $node->type->name) {
+ $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext);
+ if ($type instanceof CollectionType) {
+ return $type->getCollectionKeyType();
+ }
+
+ throw new \DomainException(\sprintf('"%s" is not a valid type for "key-of".', $node->genericTypes[0]));
+ }
+
$type = $this->getTypeFromNode($node->type, $typeContext);
// handle integer ranges as simple integers
diff --git a/src/Symfony/Component/Uid/UuidV7.php b/src/Symfony/Component/Uid/UuidV7.php
index 0be7fcb341b09..12c7520953b11 100644
--- a/src/Symfony/Component/Uid/UuidV7.php
+++ b/src/Symfony/Component/Uid/UuidV7.php
@@ -60,7 +60,7 @@ public static function generate(?\DateTimeInterface $time = null): string
if ($time > self::$time || (null !== $mtime && $time !== self::$time)) {
randomize:
- self::$rand = unpack('n*', isset(self::$seed) ? random_bytes(10) : self::$seed = random_bytes(16));
+ self::$rand = unpack('S*', isset(self::$seed) ? random_bytes(10) : self::$seed = random_bytes(16));
self::$rand[1] &= 0x03FF;
self::$time = $time;
} else {
@@ -76,7 +76,7 @@ public static function generate(?\DateTimeInterface $time = null): string
// 24-bit number in the self::$seedParts list and decrement self::$seedIndex.
if (!self::$seedIndex) {
- $s = unpack('l*', self::$seed = hash('sha512', self::$seed, true));
+ $s = unpack(\PHP_INT_SIZE >= 8 ? 'L*' : 'l*', self::$seed = hash('sha512', self::$seed, true));
$s[] = ($s[1] >> 8 & 0xFF0000) | ($s[2] >> 16 & 0xFF00) | ($s[3] >> 24 & 0xFF);
$s[] = ($s[4] >> 8 & 0xFF0000) | ($s[5] >> 16 & 0xFF00) | ($s[6] >> 24 & 0xFF);
$s[] = ($s[7] >> 8 & 0xFF0000) | ($s[8] >> 16 & 0xFF00) | ($s[9] >> 24 & 0xFF);
diff --git a/src/Symfony/Component/Validator/Constraints/Cascade.php b/src/Symfony/Component/Validator/Constraints/Cascade.php
index 8879ca657311a..4f0efd6af3681 100644
--- a/src/Symfony/Component/Validator/Constraints/Cascade.php
+++ b/src/Symfony/Component/Validator/Constraints/Cascade.php
@@ -32,6 +32,7 @@ public function __construct(array|string|null $exclude = null, ?array $options =
{
if (\is_array($exclude) && !array_is_list($exclude)) {
$options = array_merge($exclude, $options ?? []);
+ $options['exclude'] = array_flip((array) ($options['exclude'] ?? []));
} else {
$this->exclude = array_flip((array) $exclude);
}
diff --git a/src/Symfony/Component/Validator/Constraints/CollectionValidator.php b/src/Symfony/Component/Validator/Constraints/CollectionValidator.php
index a44694345aab0..7d5b20bf16ec6 100644
--- a/src/Symfony/Component/Validator/Constraints/CollectionValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/CollectionValidator.php
@@ -47,7 +47,6 @@ public function validate(mixed $value, Constraint $constraint): void
$context = $this->context;
foreach ($constraint->fields as $field => $fieldConstraint) {
- // bug fix issue #2779
$existsInArray = \is_array($value) && \array_key_exists($field, $value);
$existsInArrayAccess = $value instanceof \ArrayAccess && $value->offsetExists($field);
diff --git a/src/Symfony/Component/Validator/Constraints/LocaleValidator.php b/src/Symfony/Component/Validator/Constraints/LocaleValidator.php
index 87d0b26794e71..7f1bfe2651550 100644
--- a/src/Symfony/Component/Validator/Constraints/LocaleValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/LocaleValidator.php
@@ -44,7 +44,7 @@ public function validate(mixed $value, Constraint $constraint): void
$value = \Locale::canonicalize($value);
}
- if (!Locales::exists($value)) {
+ if (null === $value || !Locales::exists($value)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($inputValue))
->setCode(Locale::NO_SUCH_LOCALE_ERROR)
diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf
index 2a2e559b95238..d5f48f0ae7ff2 100644
--- a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf
+++ b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf
@@ -468,7 +468,7 @@
This value is not a valid Twig template.
- Tato hodnota není platná šablona Twig.
+ Tato hodnota není platná Twig šablona.