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. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf index a6be16580c6bd..0acf6dbf23a6c 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf @@ -468,7 +468,7 @@ This value is not a valid Twig template. - Este valor não é um modelo Twig válido. + Este valor não é um modelo Twig válido. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CascadeTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CascadeTest.php index ee3798079dc39..2ef4c9c83c549 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CascadeTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CascadeTest.php @@ -27,6 +27,20 @@ public function testCascadeAttribute() self::assertTrue($loader->loadClassMetadata($metadata)); self::assertSame(CascadingStrategy::CASCADE, $metadata->getCascadingStrategy()); } + + public function testExcludeProperties() + { + $constraint = new Cascade(['foo', 'bar']); + + self::assertSame(['foo' => 0, 'bar' => 1], $constraint->exclude); + } + + public function testExcludePropertiesDoctrineStyle() + { + $constraint = new Cascade(['exclude' => ['foo', 'bar']]); + + self::assertSame(['foo' => 0, 'bar' => 1], $constraint->exclude); + } } #[Cascade] diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php index 1626eecef48ff..d58bc6c888b22 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php @@ -91,6 +91,21 @@ public static function getInvalidLocales() ]; } + public function testTooLongLocale() + { + $constraint = new Locale([ + 'message' => 'myMessage', + ]); + + $locale = str_repeat('a', (\defined('INTL_MAX_LOCALE_LEN') ? \INTL_MAX_LOCALE_LEN : 85) + 1); + $this->validator->validate($locale, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"' . $locale . '"') + ->setCode(Locale::NO_SUCH_LOCALE_ERROR) + ->assertRaised(); + } + /** * @dataProvider getUncanonicalizedLocales */ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php index d755df486e140..5abb7487ba328 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php @@ -63,6 +63,19 @@ public function testValidMac($mac) $this->assertNoViolation(); } + /** + * @dataProvider getNotValidMacs + */ + public function testNotValidMac($mac) + { + $this->validator->validate($mac, new MacAddress()); + + $this->buildViolation('This value is not a valid MAC address.') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + public static function getValidMacs(): array { return [ @@ -76,6 +89,17 @@ public static function getValidMacs(): array ]; } + public static function getNotValidMacs(): array + { + return [ + ['00:00:00:00:00'], + ['00:00:00:00:00:0G'], + ['GG:GG:GG:GG:GG:GG'], + ['GG-GG-GG-GG-GG-GG'], + ['GGGG.GGGG.GGGG'], + ]; + } + public static function getValidLocalUnicastMacs(): array { return [ diff --git a/src/Symfony/Component/VarDumper/Caster/PgSqlCaster.php b/src/Symfony/Component/VarDumper/Caster/PgSqlCaster.php index 3e759f69b6798..e826412e85d29 100644 --- a/src/Symfony/Component/VarDumper/Caster/PgSqlCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/PgSqlCaster.php @@ -14,7 +14,7 @@ use Symfony\Component\VarDumper\Cloner\Stub; /** - * Casts pqsql resources to array representation. + * Casts pgsql resources to array representation. * * @author Nicolas Grekas * @@ -133,9 +133,9 @@ public static function castResult($result, array $a, Stub $stub, bool $isNested) 'name' => pg_field_name($result, $i), 'table' => \sprintf('%s (OID: %s)', pg_field_table($result, $i), pg_field_table($result, $i, true)), 'type' => \sprintf('%s (OID: %s)', pg_field_type($result, $i), pg_field_type_oid($result, $i)), - 'nullable' => (bool) pg_field_is_null($result, $i), + 'nullable' => (bool) (\PHP_VERSION_ID >= 80300 ? pg_field_is_null($result, null, $i) : pg_field_is_null($result, $i)), 'storage' => pg_field_size($result, $i).' bytes', - 'display' => pg_field_prtlen($result, $i).' chars', + 'display' => (\PHP_VERSION_ID >= 80300 ? pg_field_prtlen($result, null, $i) : pg_field_prtlen($result, $i)).' chars', ]; if (' (OID: )' === $field['table']) { $field['table'] = null; diff --git a/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php b/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php index d8422e0558048..f514ddb87cf16 100644 --- a/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php @@ -78,12 +78,14 @@ public static function castLazyObjectState($state, array $a, Stub $stub, bool $i $instance = $a['realInstance'] ?? null; - $a = ['status' => new ConstStub(match ($a['status']) { - LazyObjectState::STATUS_INITIALIZED_FULL => 'INITIALIZED_FULL', - LazyObjectState::STATUS_INITIALIZED_PARTIAL => 'INITIALIZED_PARTIAL', - LazyObjectState::STATUS_UNINITIALIZED_FULL => 'UNINITIALIZED_FULL', - LazyObjectState::STATUS_UNINITIALIZED_PARTIAL => 'UNINITIALIZED_PARTIAL', - }, $a['status'])]; + if (isset($a['status'])) { // forward-compat with Symfony 8 + $a = ['status' => new ConstStub(match ($a['status']) { + LazyObjectState::STATUS_INITIALIZED_FULL => 'INITIALIZED_FULL', + LazyObjectState::STATUS_INITIALIZED_PARTIAL => 'INITIALIZED_PARTIAL', + LazyObjectState::STATUS_UNINITIALIZED_FULL => 'UNINITIALIZED_FULL', + LazyObjectState::STATUS_UNINITIALIZED_PARTIAL => 'UNINITIALIZED_PARTIAL', + }, $a['status'])]; + } if ($instance) { $a['realInstance'] = $instance; diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index 34ef66e654c5b..ff663f91fccf8 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -744,7 +744,7 @@ private static function evaluateScalar(string $scalar, int $flags, array &$refer if (false !== $scalar = $time->getTimestamp()) { return $scalar; } - } catch (\ValueError) { + } catch (\DateRangeError|\ValueError) { // no-op } pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy