diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 72ea7cfa3e9c0..f7b87a1ce8cd4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 6.3 for features / 5.4 or 6.2 for bug fixes +| Branch? | 6.4 for features / 5.4, 6.2, or 6.3 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 87cd21cf0d442..b66e1b53b60f4 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -6,7 +6,7 @@ on: schedule: - cron: '34 4 * * 6' push: - branches: [ "6.3" ] + branches: [ "6.4" ] # Declare default permissions as read only. permissions: read-all diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 358008506bff3..86206dae8d6ad 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -64,7 +64,7 @@ jobs: echo COLUMNS=120 >> $GITHUB_ENV echo PHPUNIT="$(pwd)/phpunit --exclude-group tty,benchmark,intl-data,integration" >> $GITHUB_ENV - echo COMPOSER_UP='composer update --no-progress --ansi'$([[ "${{ matrix.php }}" = "8.2" ]] && echo ' --ignore-platform-req=php+') >> $GITHUB_ENV + echo COMPOSER_UP='composer update --no-progress --ansi'$([[ "${{ matrix.mode }}" = experimental ]] && 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) SYMFONY_VERSION=$(grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | cut -d "'" -f2 | cut -d '.' -f 1-2) diff --git a/CHANGELOG-6.2.md b/CHANGELOG-6.2.md index d685302315bcb..5a003a6343fe2 100644 --- a/CHANGELOG-6.2.md +++ b/CHANGELOG-6.2.md @@ -7,6 +7,42 @@ in 6.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/v6.2.0...v6.2.1 +* 6.2.11 (2023-05-27) + + * bug #50442 [SecurityBundle] Update security-1.0.xsd to include missing access-token definition (aegypius) + * bug #50429 [Console] block input stream if needed (joelwurtz) + * bug #50312 [Security] Skip clearing CSRF Token on stateless logout (chalasr) + * bug #50315 [Translation] Fix handling of null messages in `ArrayLoader` (rob006) + * bug #50338 [Console] Remove ``exec`` and replace it by ``shell_exec`` (maxbeckers) + * bug #50193 [Serializer] Fix `SerializedPath` not working with constructor arguments (HypeMC) + * bug #50280 [PropertyAccess] Fix nullsafe operator on array index (HypeMC) + * bug #50362 [FrameworkBundle] Fix Workflow without a marking store definition uses marking store definition of previously defined workflow (krciga22) + * bug #50309 [HttpFoundation] UrlHelper is now aware of RequestContext changes (giosh94mhz) + * bug #50309 [HttpFoundation] UrlHelper is now aware of RequestContext changes (giosh94mhz) + * bug #50352 [Notifier][TurboSMS] Fix get sender name (ZhukV) + * bug #50354 [Process] Stop the process correctly even if underlying input stream is not closed (joelwurtz) + * bug #50332 [PropertyInfo] Fix `PhpStanExtractor` when constructor has no docblock (HypeMC) + * bug #50253 [FrameworkBundle] Generate caches consistently on successive run of `cache:clear` command (Okhoshi) + * bug #49063 [Messenger] Respect `isRetryable` decision of the retry strategy for re-delivery (FlyingDR) + * bug #50251 [Serializer] Handle datetime deserialization in U format (tugmaks) + * bug #50266 [HttpFoundation] Fix file streaming after connection aborted (rlshukhov) + * bug #50277 [Messenger] Add `IS_REPEATABLE` flag to `AsMessageHandler` attribute (adrianguenter) + * bug #50269 Fix param type annotation (l-vo) + * bug #50268 Allow resources in Query::setParam (l-vo) + * bug #50256 [HttpClient] Fix setting duplicate-name headers when redirecting with AmpHttpClient (nicolas-grekas) + * bug #50214 [WebProfilerBundle] Remove legacy filters remnants (MatTheCat) + * bug #50235 [HttpClient] Fix getting through proxies via CONNECT (nicolas-grekas) + * bug #50241 [HttpKernel] Prevent initialising lazy services during services reset (tucksaun) + * bug #50244 [HttpKernel] Fix restoring surrogate content from cache (nicolas-grekas) + * bug #50246 [DependencyInjection] Do not check errored definitions’ type (MatTheCat) + * bug #49557 [PropertyInfo] Fix phpDocExtractor nullable array value type (fabpot) + * bug #50213 [ErrorHandler] Prevent conflicts with WebProfilerBundle’s JavaScript (MatTheCat) + * bug #50192 [Serializer] backed enum throw notNormalizableValueException outside construct method (alli83) + * bug #50238 [HttpKernel] Don't use eval() to render ESI/SSI (nicolas-grekas) + * bug #50226 [HttpClient] Ensure HttplugClient ignores invalid HTTP headers (nicolas-grekas) + * bug #50203 [Messenger] Fix registering message handlers (nicolas-grekas) + * bug #50204 [ErrorHandler] Skip Httplug deprecations for HttplugClient (nicolas-grekas) + * 6.2.10 (2023-04-28) * bug #50143 [Console] trim(): Argument #1 () must be of type string, bool given (danepowell) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4f274c985c33f..24afd64907140 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -16,8 +16,8 @@ The Symfony Connect username in parenthesis allows to get more information - Grégoire Pineau (lyrixx) - Wouter de Jong (wouterj) - Maxime Steinhausser (ogizanagi) - - Kévin Dunglas (dunglas) - Christophe Coevoet (stof) + - Kévin Dunglas (dunglas) - Jordi Boggiano (seldaek) - Roland Franssen (ro0) - Victor Berchet (victor) @@ -41,8 +41,8 @@ The Symfony Connect username in parenthesis allows to get more information - Jan Schädlich (jschaedl) - Lukas Kahwe Smith (lsmith) - Jérôme Tamarelle (gromnan) - - Martin Hasoň (hason) - Kevin Bond (kbond) + - Martin Hasoň (hason) - Jeremy Mikola (jmikola) - Jean-François Simon (jfsimon) - Benjamin Eberlei (beberlei) @@ -56,21 +56,21 @@ The Symfony Connect username in parenthesis allows to get more information - Antoine Makdessi (amakdessi) - Laurent VOULLEMIER (lvo) - Pierre du Plessis (pierredup) + - Antoine Lamirault (alamirault) - Grégoire Paris (greg0ire) - Jonathan Wage (jwage) - - Antoine Lamirault (alamirault) - Titouan Galopin (tgalopin) - David Maicher (dmaicher) - - Alexander Schranz (alexander-schranz) - Gábor Egyed (1ed) - - Alexandre Salomé (alexandresalome) - Mathieu Santostefano (welcomattic) + - Alexander Schranz (alexander-schranz) + - Alexandre Salomé (alexandresalome) - William DURAND + - Mathieu Lechat (mat_the_cat) - ornicar - Dany Maillard (maidmaid) - Eriksen Costa - Diego Saint Esteben (dosten) - - Mathieu Lechat (mat_the_cat) - stealth35 ‏ (stealth35) - Alexander Mols (asm89) - Francis Besset (francisbesset) @@ -79,8 +79,8 @@ The Symfony Connect username in parenthesis allows to get more information - Iltar van der Berg - Miha Vrhovnik (mvrhov) - Mathieu Piot (mpiot) - - Saša Stamenković (umpirsky) - Vincent Langlet (deviling) + - Saša Stamenković (umpirsky) - Alex Pott - Guilhem N (guilhemn) - Vladimir Reznichenko (kalessil) @@ -90,26 +90,26 @@ The Symfony Connect username in parenthesis allows to get more information - Bilal Amarni (bamarni) - Eriksen Costa - Florin Patan (florinpatan) + - Konstantin Myakshin (koc) - Peter Rehm (rpet) + - Ruud Kamphuis (ruudk) - Henrik Bjørnskov (henrikbjorn) - David Buchmann (dbu) - - Ruud Kamphuis (ruudk) - - Konstantin Myakshin (koc) - Andrej Hudec (pulzarraider) - Julien Falque (julienfalque) - Massimiliano Arione (garak) + - Jáchym Toušek (enumag) - Douglas Greenshields (shieldo) - Christian Raue - Fran Moreno (franmomu) - - Jáchym Toušek (enumag) - Mathias Arlaud (mtarld) - Graham Campbell (graham) - Michel Weimerskirch (mweimerskirch) - Eric Clemmons (ericclemmons) - Issei Murasawa (issei_m) - Malte Schlüter (maltemaltesich) - - Vasilij Dusko - Denis (yethee) + - Vasilij Dusko - Arnout Boks (aboks) - Charles Sarrazin (csarrazi) - Przemysław Bogusz (przemyslaw-bogusz) @@ -118,8 +118,10 @@ The Symfony Connect username in parenthesis allows to get more information - Maxime Helias (maxhelias) - Ener-Getick - Sebastiaan Stok (sstok) + - Tugdual Saunier (tucksaun) - Jérôme Vasseur (jvasseur) - Ion Bazan (ionbazan) + - Rokas Mikalkėnas (rokasm) - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) @@ -131,7 +133,6 @@ The Symfony Connect username in parenthesis allows to get more information - Smaine Milianni (ismail1432) - John Wards (johnwards) - Dariusz Ruminski - - Rokas Mikalkėnas (rokasm) - Lars Strojny (lstrojny) - Antoine Hérault (herzult) - Konstantin.Myakshin @@ -147,7 +148,6 @@ The Symfony Connect username in parenthesis allows to get more information - Andreas Braun - Teoh Han Hui (teohhanhui) - YaFou - - Tugdual Saunier (tucksaun) - Gary PEGEOT (gary-p) - Chris Wilkinson (thewilkybarkid) - Brice BERNARD (brikou) @@ -163,6 +163,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jeroen Spee (jeroens) - Michael Babker (mbabker) - Włodzimierz Gajda (gajdaw) + - Hugo Alliaume (kocal) - Christian Scheb - Guillaume (guill) - Christopher Hertel (chertel) @@ -171,7 +172,6 @@ The Symfony Connect username in parenthesis allows to get more information - Olivier Dolbeau (odolbeau) - Florian Voutzinos (florianv) - zairig imad (zairigimad) - - Hugo Alliaume (kocal) - Colin Frei - Javier Spagnoletti (phansys) - excelwebzone @@ -260,6 +260,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sokolov Evgeniy (ewgraf) - Stadly - Justin Hileman (bobthecow) + - Bastien Jaillot (bastnic) - Tom Van Looy (tvlooy) - Niels Keurentjes (curry684) - Vyacheslav Pavlov @@ -281,7 +282,6 @@ The Symfony Connect username in parenthesis allows to get more information - Filippo Tessarotto (slamdunk) - 77web - Bohan Yang (brentybh) - - Bastien Jaillot (bastnic) - W0rma - Matthieu Ouellette-Vachon (maoueh) - Lynn van der Berg (kjarli) @@ -293,6 +293,7 @@ The Symfony Connect username in parenthesis allows to get more information - Tyson Andre - GDIBass - Samuel NELA (snela) + - Romain Monteil (ker0x) - dFayet - gnito-org - Karoly Gossler (connorhu) @@ -327,7 +328,6 @@ The Symfony Connect username in parenthesis allows to get more information - D (denderello) - Jonathan Scheiber (jmsche) - DQNEO - - Romain Monteil (ker0x) - Andrii Bodnar - Artem (artemgenvald) - ivan @@ -377,6 +377,7 @@ The Symfony Connect username in parenthesis allows to get more information - Hidde Wieringa (hiddewie) - Christopher Davis (chrisguitarguy) - Lukáš Holeczy (holicz) + - Michael Lee (zerustech) - Florian Lonqueu-Brochard (florianlb) - Leszek Prabucki (l3l0) - Emanuele Panzeri (thepanz) @@ -418,15 +419,18 @@ The Symfony Connect username in parenthesis allows to get more information - Antonio Jose Cerezo (ajcerezo) - Marcin Szepczynski (czepol) - Lescot Edouard (idetox) + - Loïc Frémont (loic425) - Rob Frawley 2nd (robfrawley) - Mohammad Emran Hasan (phpfour) + - Allison Guilhem (a_guilhem) - Dmitriy Mamontov (mamontovdmitriy) + - Kévin THERAGE (kevin_therage) - Nikita Konstantinov (unkind) - - Michael Lee (zerustech) - Dariusz - Francois Zaninotto - Laurent Masforné (heisenberg) - Claude Khedhiri (ck-developer) + - Giorgio Premi - Daniel Tschinder - Christian Schmidt - Alexander Kotynia (olden) @@ -507,20 +511,18 @@ The Symfony Connect username in parenthesis allows to get more information - Frank de Jonge - Chris Tanaskoski - julien57 - - Loïc Frémont (loic425) + - Renan (renanbr) - Ippei Sumida (ippey_s) - Ben Ramsey (ramsey) - - Allison Guilhem (a_guilhem) - Matthieu Auger (matthieuauger) - - Kévin THERAGE (kevin_therage) - Josip Kruslin (jkruslin) - - Giorgio Premi - renanbr - Maxim Dovydenok (shiftby) - Sébastien Lavoie (lavoiesl) - Alex Rock (pierstoval) - Wodor Wodorski - Beau Simensen (simensen) + - Magnus Nordlander (magnusnordlander) - Robert Kiss (kepten) - Zan Baldwin (zanbaldwin) - Antonio J. García Lagar (ajgarlag) @@ -535,10 +537,12 @@ The Symfony Connect username in parenthesis allows to get more information - Pascal Luna (skalpa) - Wouter Van Hecke - Michael Holm (hollo) + - Yassine Guedidi (yguedidi) - Giso Stallenberg (gisostallenberg) - Blanchon Vincent (blanchonvincent) - William Arslett (warslett) - Jérémy REYNAUD (babeuloula) + - Daniel Burger - Christian Schmidt - Gonzalo Vilaseca (gonzalovilaseca) - Vadim Borodavko (javer) @@ -596,6 +600,7 @@ The Symfony Connect username in parenthesis allows to get more information - Emanuele Gaspari (inmarelibero) - Dariusz Rumiński - Terje Bråten + - Florent Morselli (spomky_) - Gennadi Janzen - James Hemery - Egor Taranov @@ -609,6 +614,7 @@ The Symfony Connect username in parenthesis allows to get more information - Khoo Yong Jun - Christin Gruber (christingruber) - Jeremy Livingston (jeremylivingston) + - Tobias Bönner - Julien Turby - scyzoryck - Greg Anderson @@ -631,7 +637,6 @@ The Symfony Connect username in parenthesis allows to get more information - Angelov Dejan (angelov) - DT Inier (gam6itko) - Matthew Lewinski (lewinski) - - Magnus Nordlander (magnusnordlander) - Ricard Clau (ricardclau) - Dmitrii Tarasov (dtarasov) - Philipp Kolesnikov @@ -703,6 +708,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jelle Raaijmakers (gmta) - Roberto Nygaard - Joshua Nye + - Jordane VASPARD (elementaire) - Dalibor Karlović - Randy Geraads - Sanpi (sanpi) @@ -746,6 +752,7 @@ The Symfony Connect username in parenthesis allows to get more information - Hassan Amouhzi - Antonin CLAUZIER (0x346e3730) - Andrei C. (moldman) + - Samaël Villette (samadu61) - Tamas Szijarto - stlrnz - Adrien Wilmet (adrienfr) @@ -830,7 +837,6 @@ The Symfony Connect username in parenthesis allows to get more information - Sebastian Paczkowski (sebpacz) - Dragos Protung (dragosprotung) - Thiago Cordeiro (thiagocordeiro) - - Florent Morselli (spomky_) - Julien Maulny - Brian King - Paul Oms @@ -845,9 +851,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jon Gotlin (jongotlin) - Jeanmonod David (jeanmonod) - Daniel González (daniel.gonzalez) - - Renan (renanbr) - Webnet team (webnet) - - Tobias Bönner - Berny Cantos (xphere81) - Mátyás Somfai (smatyas) - Simon Leblanc (leblanc_simon) @@ -856,6 +860,7 @@ The Symfony Connect username in parenthesis allows to get more information - Niklas Fiekas - Mark Challoner (markchalloner) - Markus Bachmann (baachi) + - Matthieu Lempereur (mryamous) - Roger Guasch (rogerguasch) - Luis Tacón (lutacon) - Alex Hofbauer (alexhofbauer) @@ -863,7 +868,9 @@ The Symfony Connect username in parenthesis allows to get more information - lancergr - Ivan Nikolaev (destillat) - Xavier Leune (xleune) + - Matthieu Calie (matth--) - Ben Roberts (benr77) + - Benjamin Georgeault (wedgesama) - Joost van Driel (j92) - ampaze - Arturs Vonda @@ -900,7 +907,6 @@ The Symfony Connect username in parenthesis allows to get more information - Adam Harvey - ilyes kooli (skafandri) - Anton Bakai - - Daniel Burger - Sam Fleming (sam_fleming) - Alex Bakhturin - Brayden Williams (redstar504) @@ -944,6 +950,7 @@ The Symfony Connect username in parenthesis allows to get more information - mcben - Jérôme Vieilledent (lolautruche) - Filip Procházka (fprochazka) + - Alex Kalineskou - stoccc - Markus Lanthaler (lanthaler) - Gigino Chianese (sajito) @@ -1015,11 +1022,9 @@ The Symfony Connect username in parenthesis allows to get more information - Matthias Schmidt - Lenar Lõhmus - Ilija Tovilo (ilijatovilo) - - Samaël Villette (samadu61) - Zach Badgett (zachbadgett) - Loïc Faugeron - Aurélien Fredouelle - - Jordane VASPARD (elementaire) - Pavel Campr (pcampr) - Forfarle (forfarle) - Johnny Robeson (johnny) @@ -1101,7 +1106,6 @@ The Symfony Connect username in parenthesis allows to get more information - Giuseppe Campanelli - Valentin - pizzaminded - - Matthieu Calie (matth--) - Stéphane Escandell (sescandell) - ivan - linh @@ -1140,7 +1144,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jacek Wilczyński (jacekwilczynski) - Hany el-Kerdany - Wang Jingyu - - Benjamin Georgeault (wedgesama) - Åsmund Garfors - Maxime Douailin - Jean Pasdeloup @@ -1163,6 +1166,7 @@ The Symfony Connect username in parenthesis allows to get more information - Łukasz Chruściel (lchrusciel) - Jan Vernieuwe (vernija) - zenmate + - Cédric Anne - j.schmitt - Georgi Georgiev - David Fuhr @@ -1171,6 +1175,7 @@ The Symfony Connect username in parenthesis allows to get more information - mwos - Aurimas Niekis (gcds) - Volker Killesreiter (ol0lll) + - Benjamin Zaslavsky (tiriel) - Vedran Mihočinec (v-m-i) - creiner - RevZer0 (rav) @@ -1212,6 +1217,7 @@ The Symfony Connect username in parenthesis allows to get more information - Atthaphon Urairat - Jon Green (jontjs) - Mickaël Isaert (misaert) + - alexandre.lassauge - Israel J. Carberry - Julius Kiekbusch - Miquel Rodríguez Telep (mrtorrent) @@ -1261,6 +1267,7 @@ The Symfony Connect username in parenthesis allows to get more information - Szijarto Tamas - Arend Hummeling - Makdessi Alex + - Phil E. Taylor (philetaylor) - Juan Miguel Besada Vidal (soutlink) - dlorek - Stuart Fyfe @@ -1283,6 +1290,7 @@ The Symfony Connect username in parenthesis allows to get more information - dbrekelmans - Piet Steinhart - mousezheng + - Nicolas Dousson - Rémy LESCALLIER - Simon Schick (simonsimcity) - Victor Macko (victor_m) @@ -1466,6 +1474,7 @@ The Symfony Connect username in parenthesis allows to get more information - Barney Hanlon - Bart Wach - Jos Elstgeest + - Thorry84 - Kirill Lazarev - Serhii Smirnov - Martins Eglitis @@ -1671,7 +1680,6 @@ The Symfony Connect username in parenthesis allows to get more information - andrey1s - Abhoryo - Fabian Vogler (fabian) - - Yassine Guedidi (yguedidi) - Korvin Szanto - Simon Ackermann - Stéphan Kochen @@ -1734,6 +1742,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jörn Lang - David Marín Carreño (davefx) - Fabien LUCAS (flucas2) + - Alex (garrett) - Hidde Boomsma (hboomsma) - Johan Wilfer (johanwilfer) - Toby Griffiths (tog) @@ -1774,7 +1783,6 @@ The Symfony Connect username in parenthesis allows to get more information - florian-michael-mast - Henry Snoek - Vlad Dumitrache - - Alex Kalineskou - Derek ROTH - Jeremy Benoist - Ben Johnson @@ -1833,6 +1841,7 @@ The Symfony Connect username in parenthesis allows to get more information - vladyslavstartsev - Kévin - Marc Abramowitz + - Markus Staab - michal - Martijn Evers - Sjoerd Adema @@ -1991,6 +2000,7 @@ The Symfony Connect username in parenthesis allows to get more information - Daniel Alejandro Castro Arellano (lexcast) - Aleksandar Dimitrov (netbull) - Gary Houbre (thegarious) + - Vincent Chalamon - Thomas Jarrand - Baptiste Leduc (bleduc) - Antoine Bluchet (soyuka) @@ -2032,6 +2042,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sander Marechal - Franz Wilding (killerpoke) - Ferenczi Krisztian (fchris82) + - Artyum Petrov - Oleg Golovakhin (doc_tr) - Icode4Food (icode4food) - Radosław Benkel @@ -2050,6 +2061,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sander Coolen (scoolen) - Nicolas Le Goff (nlegoff) - Anne-Sophie Bachelard + - Gordienko Vladislav - Marvin Butkereit - Ben Oman - Chris de Kok @@ -2065,6 +2077,7 @@ The Symfony Connect username in parenthesis allows to get more information - Zachary Tong (polyfractal) - Ashura - Hryhorii Hrebiniuk + - Nsbx - Alex Plekhanov - johnstevenson - hamza @@ -2076,6 +2089,7 @@ The Symfony Connect username in parenthesis allows to get more information - Artem (digi) - boite - Silvio Ginter + - Peter Culka - MGDSoft - joris - Vadim Tyukov (vatson) @@ -2117,6 +2131,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jordi Rejas - Troy McCabe - Ville Mattila + - gstapinato - gr1ev0us - Léo VINCENT - mlazovla @@ -2141,7 +2156,6 @@ The Symfony Connect username in parenthesis allows to get more information - MARYNICH Mikhail (mmarynich-ext) - Viktor Novikov (nowiko) - Paul Mitchum (paul-m) - - Phil E. Taylor (philetaylor) - Angel Koilov (po_taka) - Dan Finnie - Ken Marfilla (marfillaster) @@ -2211,6 +2225,7 @@ The Symfony Connect username in parenthesis allows to get more information - Abderrahman DAIF (death_maker) - Yann Rabiller (einenlum) - Jochen Bayer (jocl) + - VAN DER PUTTE Guillaume (guillaume_vdp) - Patrick Carlo-Hickman - Bruno MATEU - Jeremy Bush @@ -2235,6 +2250,7 @@ The Symfony Connect username in parenthesis allows to get more information - BRAMILLE Sébastien (oktapodia) - Artem Kolesnikov (tyomo4ka) - Gustavo Adrian + - Matthias Neid - Yannick - Kuzia - Vladimir Luchaninov (luchaninov) @@ -2242,6 +2258,7 @@ The Symfony Connect username in parenthesis allows to get more information - rchoquet - v.shevelev - gitlost + - radar3301 - Taras Girnyk - Sergio - Mehrdad @@ -2311,6 +2328,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dmitri Petmanson - heccjj - Alexandre Melard + - Rafał Toboła - AlbinoDrought - Jay Klehr - Sergey Yuferev @@ -2330,11 +2348,11 @@ The Symfony Connect username in parenthesis allows to get more information - Jelte Steijaert (jelte) - David Négrier (moufmouf) - Quique Porta (quiqueporta) - - Benjamin Zaslavsky (tiriel) - Tobias Feijten (tobias93) - Andrea Quintino (dirk39) - Andreas Heigl (heiglandreas) - Tomasz Szymczyk (karion) + - Nadim AL ABDOU (nadim) - Peter Dietrich (xosofox) - Alex Vasilchenko - sez-open @@ -2365,6 +2383,7 @@ The Symfony Connect username in parenthesis allows to get more information - Andrei Igna - azine - Wojciech Zimoń + - Vladimir Melnik - Pierre Tachoire - Dawid Sajdak - Ludek Stepan @@ -2378,6 +2397,7 @@ The Symfony Connect username in parenthesis allows to get more information - karolsojko - Marco Jantke - Saem Ghani + - Claudiu Cristea - Zacharias Luiten - Sebastian Utz - Adrien Gallou (agallou) @@ -2462,6 +2482,8 @@ The Symfony Connect username in parenthesis allows to get more information - Max Summe - Ema Panz - Chihiro Adachi (chihiro-adachi) + - Thomas Trautner (thomastr) + - mfettig - Raphaëll Roussel - Tadcka - Abudarham Yuval @@ -2490,6 +2512,7 @@ The Symfony Connect username in parenthesis allows to get more information - Nicolas Eeckeloo (neeckeloo) - Andriy Prokopenko (sleepyboy) - Dariusz Ruminski + - Starfox64 - Thomas Hanke - Daniel Tschinder - Arnaud CHASSEUX @@ -2547,6 +2570,7 @@ The Symfony Connect username in parenthesis allows to get more information - Simon Neidhold - Valentin VALCIU - Jeremiah VALERIE + - Cas van Dongen - Patrik Patie Gmitter - Yannick Snobbert - Kevin Dew @@ -2574,8 +2598,8 @@ The Symfony Connect username in parenthesis allows to get more information - Kirk Madera - Keith Maika - Mephistofeles + - Oleh Korneliuk - Hoffmann András - - Cédric Anne - LubenZA - Flavian Sierk - Rik van der Heijden @@ -2595,6 +2619,7 @@ The Symfony Connect username in parenthesis allows to get more information - Olivier Scherler (oscherler) - Shane Preece (shane) - Johannes Goslar + - Mike Gladysch - Geoff - georaldc - wusuopu @@ -2667,6 +2692,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dan Blows - Matt Wells - Nicolas Appriou + - Javier Alfonso Bellota de Frutos - stloyd - Andreas - Chris Tickner @@ -2826,11 +2852,13 @@ The Symfony Connect username in parenthesis allows to get more information - Michael van Tricht - ReScO - Tim Strehle + - cay89 - Sam Ward - Hans N. Hjort - Walther Lalk - Adam - Ivo + - Markus Staab - Sören Bernstein - michael.kubovic - devel @@ -2918,6 +2946,7 @@ The Symfony Connect username in parenthesis allows to get more information - Götz Gottwald - Adrien Peyre - Christoph Krapp + - andreyserdjuk - Nick Chiu - Robert Campbell - Matt Lehner @@ -2939,7 +2968,6 @@ The Symfony Connect username in parenthesis allows to get more information - Alessandro Loffredo - Ian Phillips - Remi Collet - - Nicolas Dousson - Haritz - Matthieu Prat - Brieuc Thomas @@ -2964,6 +2992,7 @@ The Symfony Connect username in parenthesis allows to get more information - tourze - Erik van Wingerden - Valouleloup + - Roland Franssen :) - Alexis MARQUIS - Matheus Gontijo - Gerrit Drost @@ -2998,7 +3027,6 @@ The Symfony Connect username in parenthesis allows to get more information - Tomáš Polívka (draczris) - Dennis Smink (dsmink) - Franz Liedke (franzliedke) - - Alex (garrett) - Gaylord Poillon (gaylord_p) - gondo (gondo) - Joris Garonian (grifx) @@ -3034,6 +3062,7 @@ The Symfony Connect username in parenthesis allows to get more information - Yorkie Chadwick (yorkie76) - Pavel Barton - GuillaumeVerdon + - Marien Fressinaud - ureimers - akimsko - Youpie @@ -3175,8 +3204,8 @@ The Symfony Connect username in parenthesis allows to get more information - Arrilot - andrey-tech - Shaun Simmons - - Markus Staab - Pierre-Louis LAUNAY + - A. Pauly - djama - Michael Gwynne - Eduardo Conceição @@ -3242,6 +3271,7 @@ The Symfony Connect username in parenthesis allows to get more information - Alfonso Fernández García - phc - Дмитрий Пацура + - db306 - Michaël VEROUX - Julia - Lin Lu diff --git a/composer.json b/composer.json index 5c338d388cfd0..18539cf8b7540 100644 --- a/composer.json +++ b/composer.json @@ -136,13 +136,14 @@ "nyholm/psr7": "^1.0", "pda/pheanstalk": "^4.0", "php-http/httplug": "^1.0|^2.0", + "php-http/message-factory": "^1.0", "phpdocumentor/reflection-docblock": "^5.2", "phpstan/phpdoc-parser": "^1.0", "predis/predis": "~1.1", "psr/http-client": "^1.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "symfony/mercure-bundle": "^0.3", - "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", "symfony/runtime": "self.version", "symfony/security-acl": "~2.8|~3.0", "twig/cssinliner-extra": "^2.12|^3", @@ -158,10 +159,11 @@ "masterminds/html5": "<2.6", "phpdocumentor/reflection-docblock": "<5.2", "phpdocumentor/type-resolver": "<1.5.1", - "phpunit/phpunit": "<5.4.3" + "phpunit/phpunit": "<7.5|9.1.2" }, "config": { "allow-plugins": { + "php-http/discovery": false, "symfony/runtime": true } }, diff --git a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php index 9813d712132f4..695a0a1535aa2 100644 --- a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php +++ b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php @@ -43,7 +43,7 @@ public function stop(): void } } - public function setParam(string|int $param, null|string|int|float|bool &$variable, int $type): void + public function setParam(string|int $param, mixed &$variable, int $type): void { // Numeric indexes start at 0 in profiler $idx = \is_int($param) ? $param - 1 : $param; diff --git a/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php index e605afec5a630..2bb628442459c 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php @@ -145,23 +145,31 @@ public function testWithParamBound(callable $executeMethod) { $this->init(); - $product = 'product1'; - $price = 12.5; - $stock = 5; + $sql = <<getResourceFromString('mydata'); - $stmt = $this->conn->prepare('INSERT INTO products(name, price, stock) VALUES (?, ?, ?)'); + $stmt = $this->conn->prepare($sql); $stmt->bindParam(1, $product); $stmt->bindParam(2, $price); $stmt->bindParam(3, $stock, ParameterType::INTEGER); + $stmt->bindParam(4, $res, ParameterType::BINARY); + + $product = 'product1'; + $price = 12.5; + $stock = 5; $executeMethod($stmt); // Debug data should not be affected by these changes $debug = $this->debugDataHolder->getData()['default'] ?? []; $this->assertCount(2, $debug); - $this->assertSame('INSERT INTO products(name, price, stock) VALUES (?, ?, ?)', $debug[1]['sql']); - $this->assertSame(['product1', '12.5', 5], $debug[1]['params']); - $this->assertSame([ParameterType::STRING, ParameterType::STRING, ParameterType::INTEGER], $debug[1]['types']); + $this->assertSame($sql, $debug[1]['sql']); + $this->assertSame(['product1', 12.5, 5, $expectedRes], $debug[1]['params']); + $this->assertSame([ParameterType::STRING, ParameterType::STRING, ParameterType::INTEGER, ParameterType::BINARY], $debug[1]['types']); $this->assertGreaterThan(0, $debug[1]['executionMS']); } diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 7430758321d3e..abfa858cf037f 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -53,7 +53,6 @@ "doctrine/dbal": "<2.13.1", "doctrine/lexer": "<1.1", "doctrine/orm": "<2.7.4", - "phpunit/phpunit": "<5.4.3", "symfony/cache": "<5.4", "symfony/dependency-injection": "<5.4", "symfony/form": "<5.4.21|>=6,<6.2.7", diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index ca46a577154a3..6a4ae1aefb5e9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -129,14 +129,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($output->isVerbose()) { $io->comment('Warming up optional cache...'); } - $warmer = $kernel->getContainer()->get('cache_warmer'); - // non optional warmers already ran during container compilation - $warmer->enableOnlyOptionalWarmers(); - $preload = (array) $warmer->warmUp($realCacheDir); - - if ($preload && file_exists($preloadFile = $realCacheDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) { - Preloader::append($preloadFile, $preload); - } + $this->warmupOptionals($realCacheDir); } } else { $fs->mkdir($warmupDir); @@ -145,7 +138,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($output->isVerbose()) { $io->comment('Warming up cache...'); } - $this->warmup($warmupDir, $realCacheDir, !$input->getOption('no-optional-warmers')); + $this->warmup($warmupDir, $realBuildDir); + + if (!$input->getOption('no-optional-warmers')) { + if ($output->isVerbose()) { + $io->comment('Warming up optional cache...'); + } + $this->warmupOptionals($realCacheDir); + } } if (!$fs->exists($warmupDir.'/'.$containerDir)) { @@ -219,7 +219,7 @@ private function isNfs(string $dir): bool return false; } - private function warmup(string $warmupDir, string $realBuildDir, bool $enableOptionalWarmers = true) + private function warmup(string $warmupDir, string $realBuildDir): void { // create a temporary kernel $kernel = $this->getApplication()->getKernel(); @@ -228,18 +228,6 @@ private function warmup(string $warmupDir, string $realBuildDir, bool $enableOpt } $kernel->reboot($warmupDir); - // warmup temporary dir - if ($enableOptionalWarmers) { - $warmer = $kernel->getContainer()->get('cache_warmer'); - // non optional warmers already ran during container compilation - $warmer->enableOnlyOptionalWarmers(); - $preload = (array) $warmer->warmUp($warmupDir); - - if ($preload && file_exists($preloadFile = $warmupDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) { - Preloader::append($preloadFile, $preload); - } - } - // fix references to cached files with the real cache directory name $search = [$warmupDir, str_replace('\\', '\\\\', $warmupDir)]; $replace = str_replace('\\', '/', $realBuildDir); @@ -250,4 +238,17 @@ private function warmup(string $warmupDir, string $realBuildDir, bool $enableOpt } } } + + private function warmupOptionals(string $realCacheDir): void + { + $kernel = $this->getApplication()->getKernel(); + $warmer = $kernel->getContainer()->get('cache_warmer'); + // non optional warmers already ran during container compilation + $warmer->enableOnlyOptionalWarmers(); + $preload = (array) $warmer->warmUp($realCacheDir); + + if ($preload && file_exists($preloadFile = $realCacheDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) { + Preloader::append($preloadFile, $preload); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php index a69624c8372c4..9e6ef9330e24a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php @@ -92,12 +92,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($errors) { $io->error('Done but with errors.'); - return self::FAILURE; + return 1; } $io->success('Successfully invalidated cache tags.'); - return self::SUCCESS; + return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 7bf72ee6c801f..4fea9249a2f14 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -970,6 +970,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $definitionDefinition->addArgument(new Reference(sprintf('%s.metadata_store', $workflowId))); // Create MarkingStore + $markingStoreDefinition = null; if (isset($workflow['marking_store']['type'])) { $markingStoreDefinition = new ChildDefinition('workflow.marking_store.method'); $markingStoreDefinition->setArguments([ @@ -983,7 +984,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ // Create Workflow $workflowDefinition = new ChildDefinition(sprintf('%s.abstract', $type)); $workflowDefinition->replaceArgument(0, new Reference(sprintf('%s.definition', $workflowId))); - $workflowDefinition->replaceArgument(1, $markingStoreDefinition ?? null); + $workflowDefinition->replaceArgument(1, $markingStoreDefinition); $workflowDefinition->replaceArgument(3, $name); $workflowDefinition->replaceArgument(4, $workflow['events_to_dispatch']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index 9facc09c783f8..a648e8fdb1e49 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -120,7 +120,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] ->set('url_helper', UrlHelper::class) ->args([ service('request_stack'), - service('router.request_context')->ignoreOnInvalid(), + service('router')->ignoreOnInvalid(), ]) ->alias(UrlHelper::class, 'url_helper') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index 5daeaa14ec28e..506162a42cdf7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -88,6 +88,63 @@ public function testWorkflowValidationStateMachine() }); } + public function testWorkflowDefaultMarkingStoreDefinition() + { + $container = $this->createContainerFromClosure(function ($container) { + $container->loadFromExtension('framework', [ + 'http_method_override' => false, + 'workflows' => [ + 'workflow_a' => [ + 'type' => 'state_machine', + 'marking_store' => [ + 'type' => 'method', + 'property' => 'status', + ], + 'supports' => [ + __CLASS__, + ], + 'places' => [ + 'a', + 'b', + ], + 'transitions' => [ + 'a_to_b' => [ + 'from' => ['a'], + 'to' => ['b'], + ], + ], + ], + 'workflow_b' => [ + 'type' => 'state_machine', + 'supports' => [ + __CLASS__, + ], + 'places' => [ + 'a', + 'b', + ], + 'transitions' => [ + 'a_to_b' => [ + 'from' => ['a'], + 'to' => ['b'], + ], + ], + ], + ], + ]); + }); + + $workflowA = $container->getDefinition('state_machine.workflow_a'); + $argumentsA = $workflowA->getArguments(); + $this->assertArrayHasKey('index_1', $argumentsA, 'workflow_a has a marking_store argument'); + $this->assertNotNull($argumentsA['index_1'], 'workflow_a marking_store argument is not null'); + + $workflowB = $container->getDefinition('state_machine.workflow_b'); + $argumentsB = $workflowB->getArguments(); + $this->assertArrayHasKey('index_1', $argumentsB, 'workflow_b has a marking_store argument'); + $this->assertNull($argumentsB['index_1'], 'workflow_b marking_store argument is null'); + } + public function testRateLimiterWithLockFactory() { try { diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 4fc22ab7122bd..be8be43d1ca81 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -25,7 +25,7 @@ "symfony/deprecation-contracts": "^2.1|^3", "symfony/error-handler": "^6.1", "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/http-foundation": "^6.2", + "symfony/http-foundation": "^6.2.11", "symfony/http-kernel": "^6.2.1", "symfony/polyfill-mbstring": "~1.0", "symfony/filesystem": "^5.4|^6.0", @@ -74,7 +74,6 @@ "doctrine/persistence": "<1.3", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "phpunit/phpunit": "<5.4.3", "symfony/asset": "<5.4", "symfony/console": "<5.4", "symfony/dotenv": "<5.4", diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd index 33140fdae8d11..af058365cc716 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd @@ -140,6 +140,7 @@ + @@ -304,6 +305,17 @@ + + + + + + + + + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig index 13503feeb4c05..40ef496fac0ae 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig @@ -155,22 +155,20 @@ - - {% endblock messages %} {% endif %} {% endblock %} {% macro render_table(messages, is_fallback) %} - +
- + {% if is_fallback %} {% endif %} - + @@ -178,7 +176,7 @@ {% for message in messages %} - + {% if is_fallback %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 953ee53f8d2f9..1c59dd0cb5684 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -1637,42 +1637,6 @@ tr.status-warning td { display: block; } -{# Filters - ========================================================================= #} -[data-filters] { position: relative; } -[data-filtered] { cursor: pointer; } -[data-filtered]:after { content: '\00a0\25BE'; } -[data-filtered]:hover .filter-list li { display: inline-flex; } -[class*="filter-hidden-"] { display: none; } -.filter-list { position: absolute; border: var(--border); box-shadow: var(--shadow); margin: 0; padding: 0; display: flex; flex-direction: column; } -.filter-list :after { content: ''; } -.filter-list li { - background: var(--tab-disabled-background); - border-bottom: var(--border); - color: var(--tab-disabled-color); - display: none; - list-style: none; - margin: 0; - padding: 5px 10px; - text-align: left; - font-weight: normal; -} -.filter-list li.active { - background: var(--tab-background); - color: var(--tab-color); -} -.filter-list li.last-active { - background: var(--tab-active-background); - color: var(--tab-active-color); -} - -.filter-list-level li { cursor: s-resize; } -.filter-list-level li.active { cursor: n-resize; } -.filter-list-level li.last-active { cursor: default; } -.filter-list-level li.last-active:before { content: '\2714\00a0'; } -.filter-list-choice li:before { content: '\2714\00a0'; color: transparent; } -.filter-list-choice li.active:before { color: unset; } - {# Twig panel ========================================================================= #} #twig-dump pre { diff --git a/src/Symfony/Component/Console/Command/CompleteCommand.php b/src/Symfony/Component/Console/Command/CompleteCommand.php index e65b334ce89db..dbf5d7ddc3309 100644 --- a/src/Symfony/Component/Console/Command/CompleteCommand.php +++ b/src/Symfony/Component/Console/Command/CompleteCommand.php @@ -173,10 +173,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw $e; } - return self::FAILURE; + return 2; } - return self::SUCCESS; + return 0; } private function createCompletionInput(InputInterface $input): CompletionInput diff --git a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php index 10280f73abd83..cac944ec9375b 100644 --- a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php +++ b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php @@ -96,7 +96,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->getOption('debug')) { $this->tailDebugLog($commandName, $output); - return self::SUCCESS; + return 0; } $shell = $input->getArgument('shell') ?? self::guessShell(); @@ -113,12 +113,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln(sprintf('Shell not detected, Symfony shell completion only supports "%s").', implode('", "', $supportedShells))); } - return self::INVALID; + return 2; } $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile))); - return self::SUCCESS; + return 0; } private static function guessShell(): string diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index c345b4af7747f..f26ca577a22c9 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -123,7 +123,18 @@ private function doAsk(OutputInterface $output, Question $question): mixed } if (false === $ret) { + $isBlocked = stream_get_meta_data($inputStream)['blocked'] ?? true; + + if (!$isBlocked) { + stream_set_blocking($inputStream, true); + } + $ret = $this->readInput($inputStream, $question); + + if (!$isBlocked) { + stream_set_blocking($inputStream, false); + } + if (false === $ret) { throw new MissingInputException('Aborted.'); } @@ -496,13 +507,11 @@ private function isInteractiveInput($inputStream): bool return self::$stdinIsInteractive = @posix_isatty(fopen('php://stdin', 'r')); } - if (!\function_exists('exec')) { + if (!\function_exists('shell_exec')) { return self::$stdinIsInteractive = true; } - exec('stty 2> /dev/null', $output, $status); - - return self::$stdinIsInteractive = 1 !== $status; + return self::$stdinIsInteractive = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); } /** diff --git a/src/Symfony/Component/Console/Input/InputArgument.php b/src/Symfony/Component/Console/Input/InputArgument.php index a130c41226bf2..0e86e91674397 100644 --- a/src/Symfony/Component/Console/Input/InputArgument.php +++ b/src/Symfony/Component/Console/Input/InputArgument.php @@ -37,7 +37,7 @@ class InputArgument /** * @param string $name The argument name - * @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL + * @param int|null $mode The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY * @param string $description A description text * @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only) * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion diff --git a/src/Symfony/Component/Console/Terminal.php b/src/Symfony/Component/Console/Terminal.php index 7d10b13ad5991..855f4114d751c 100644 --- a/src/Symfony/Component/Console/Terminal.php +++ b/src/Symfony/Component/Console/Terminal.php @@ -123,14 +123,12 @@ public static function hasSttyAvailable(): bool return self::$stty; } - // skip check if exec function is disabled - if (!\function_exists('exec')) { + // skip check if shell_exec function is disabled + if (!\function_exists('shell_exec')) { return false; } - exec('stty 2>&1', $output, $exitcode); - - return self::$stty = 0 === $exitcode; + return self::$stty = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); } private static function initDimensions() diff --git a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php index c634f7afb80d6..6039acf571193 100644 --- a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php @@ -430,7 +430,7 @@ public function testAskHiddenResponse() $this->assertEquals('8AM', $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream("8AM\n")), $this->createOutputInterface(), $question)); } - public function testAskHiddenResponseTrimmed() + public function testAskHiddenResponseNotTrimmed() { if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('This test is not supported on Windows'); @@ -442,7 +442,7 @@ public function testAskHiddenResponseTrimmed() $question->setHidden(true); $question->setTrimmable(false); - $this->assertEquals(' 8AM', $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream(' 8AM')), $this->createOutputInterface(), $question)); + $this->assertEquals(' 8AM'.\PHP_EOL, $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream(' 8AM'.\PHP_EOL)), $this->createOutputInterface(), $question)); } public function testAskMultilineResponseWithEOF() diff --git a/src/Symfony/Component/Console/Tests/TerminalTest.php b/src/Symfony/Component/Console/Tests/TerminalTest.php index 4a9b7a74abe93..d8af7dc1ff5e6 100644 --- a/src/Symfony/Component/Console/Tests/TerminalTest.php +++ b/src/Symfony/Component/Console/Tests/TerminalTest.php @@ -77,8 +77,8 @@ public function testSttyOnWindows() $this->markTestSkipped('Must be on windows'); } - $sttyString = exec('(stty -a | grep columns) 2>&1', $output, $exitcode); - if (0 !== $exitcode) { + $sttyString = shell_exec('(stty -a | grep columns) 2> NUL'); + if (!$sttyString) { $this->markTestSkipped('Must have stty support'); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php index 26e89a9b9a26a..6da7f0852a85c 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php @@ -207,7 +207,7 @@ private function checkType(Definition $checkedDefinition, mixed $value, \Reflect $class = null; if ($value instanceof Definition) { - if ($value->getFactory()) { + if ($value->hasErrors() || $value->getFactory()) { return; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php index 2305d210ff781..c169d6c8ca7fc 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php @@ -23,6 +23,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Bar; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarErroredDependency; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgument; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgumentNotNull; @@ -980,6 +981,20 @@ public function testIgnoreDefinitionFactoryArgument() $this->addToAssertionCount(1); } + + public function testErroredDefinitionsAreNotChecked() + { + $container = new ContainerBuilder(); + $container->register('errored_dependency', BarErroredDependency::class) + ->setArguments([ + (new Definition(Foo::class)) + ->addError('error'), + ]); + + (new CheckTypeDeclarationsPass(true))->process($container); + + $this->addToAssertionCount(1); + } } class CallableClass diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarErroredDependency.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarErroredDependency.php new file mode 100644 index 0000000000000..d1368c3f7ef44 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarErroredDependency.php @@ -0,0 +1,10 @@ +checkClass($use); } if (isset(self::$deprecated[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && !isset(self::$deprecated[$class]) - && !(HttplugClient::class === $class && \in_array($use, [\Http\Message\RequestFactory::class, \Http\Message\StreamFactory::class, \Http\Message\UriFactory::class], true)) + && !(HttplugClient::class === $class && \in_array($use, [\Http\Client\HttpClient::class, \Http\Message\RequestFactory::class, \Http\Message\StreamFactory::class, \Http\Message\UriFactory::class], true)) ) { $type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait'); $verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses'); diff --git a/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js b/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js index a85409da3cc89..95b8ea17197c9 100644 --- a/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js +++ b/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js @@ -1,297 +1,285 @@ /* This file is based on WebProfilerBundle/Resources/views/Profiler/base_js.html.twig. If you make any change in this file, verify the same change is needed in the other file. */ /* .tab'); + var tabNavigation = document.createElement('ul'); + tabNavigation.className = 'tab-navigation'; + + var selectedTabId = 'tab-' + i + '-0'; /* select the first tab by default */ + for (var j = 0; j < tabs.length; j++) { + var tabId = 'tab-' + i + '-' + j; + var tabTitle = tabs[j].querySelector('.tab-title').innerHTML; + + var tabNavigationItem = document.createElement('li'); + tabNavigationItem.setAttribute('data-tab-id', tabId); + if (hasClass(tabs[j], 'active')) { selectedTabId = tabId; } + if (hasClass(tabs[j], 'disabled')) { addClass(tabNavigationItem, 'disabled'); } + tabNavigationItem.innerHTML = tabTitle; + tabNavigation.appendChild(tabNavigationItem); + + var tabContent = tabs[j].querySelector('.tab-content'); + tabContent.parentElement.setAttribute('id', tabId); + } - if (navigator.clipboard) { - document.querySelectorAll('[data-clipboard-text]').forEach(function(element) { - removeClass(element, 'hidden'); - element.addEventListener('click', function() { - navigator.clipboard.writeText(element.getAttribute('data-clipboard-text')); - }) - }); + tabGroups[i].insertBefore(tabNavigation, tabGroups[i].firstChild); + addClass(document.querySelector('[data-tab-id="' + selectedTabId + '"]'), 'active'); } - return { - addEventListener: addEventListener, + /* display the active tab and add the 'click' event listeners */ + for (i = 0; i < tabGroups.length; i++) { + tabNavigation = tabGroups[i].querySelectorAll(':scope >.tab-navigation li'); - createTabs: function() { - var tabGroups = document.querySelectorAll('.sf-tabs:not([data-processed=true])'); + for (j = 0; j < tabNavigation.length; j++) { + tabId = tabNavigation[j].getAttribute('data-tab-id'); + document.getElementById(tabId).querySelector('.tab-title').className = 'hidden'; - /* create the tab navigation for each group of tabs */ - for (var i = 0; i < tabGroups.length; i++) { - var tabs = tabGroups[i].querySelectorAll(':scope > .tab'); - var tabNavigation = document.createElement('ul'); - tabNavigation.className = 'tab-navigation'; - - var selectedTabId = 'tab-' + i + '-0'; /* select the first tab by default */ - for (var j = 0; j < tabs.length; j++) { - var tabId = 'tab-' + i + '-' + j; - var tabTitle = tabs[j].querySelector('.tab-title').innerHTML; + if (hasClass(tabNavigation[j], 'active')) { + document.getElementById(tabId).className = 'block'; + } else { + document.getElementById(tabId).className = 'hidden'; + } - var tabNavigationItem = document.createElement('li'); - tabNavigationItem.setAttribute('data-tab-id', tabId); - if (hasClass(tabs[j], 'active')) { selectedTabId = tabId; } - if (hasClass(tabs[j], 'disabled')) { addClass(tabNavigationItem, 'disabled'); } - tabNavigationItem.innerHTML = tabTitle; - tabNavigation.appendChild(tabNavigationItem); + tabNavigation[j].addEventListener('click', function(e) { + var activeTab = e.target || e.srcElement; - var tabContent = tabs[j].querySelector('.tab-content'); - tabContent.parentElement.setAttribute('id', tabId); + /* needed because when the tab contains HTML contents, user can click */ + /* on any of those elements instead of their parent '
  • ' element */ + while (activeTab.tagName.toLowerCase() !== 'li') { + activeTab = activeTab.parentNode; } - tabGroups[i].insertBefore(tabNavigation, tabGroups[i].firstChild); - addClass(document.querySelector('[data-tab-id="' + selectedTabId + '"]'), 'active'); - } + /* get the full list of tabs through the parent of the active tab element */ + var tabNavigation = activeTab.parentNode.children; + for (var k = 0; k < tabNavigation.length; k++) { + var tabId = tabNavigation[k].getAttribute('data-tab-id'); + document.getElementById(tabId).className = 'hidden'; + removeClass(tabNavigation[k], 'active'); + } - /* display the active tab and add the 'click' event listeners */ - for (i = 0; i < tabGroups.length; i++) { - tabNavigation = tabGroups[i].querySelectorAll(':scope >.tab-navigation li'); + addClass(activeTab, 'active'); + var activeTabId = activeTab.getAttribute('data-tab-id'); + document.getElementById(activeTabId).className = 'block'; + }); + } - for (j = 0; j < tabNavigation.length; j++) { - tabId = tabNavigation[j].getAttribute('data-tab-id'); - document.getElementById(tabId).querySelector('.tab-title').className = 'hidden'; + tabGroups[i].setAttribute('data-processed', 'true'); + } + })(); - if (hasClass(tabNavigation[j], 'active')) { - document.getElementById(tabId).className = 'block'; - } else { - document.getElementById(tabId).className = 'hidden'; - } + (function createToggles() { + var toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])'); - tabNavigation[j].addEventListener('click', function(e) { - var activeTab = e.target || e.srcElement; + for (var i = 0; i < toggles.length; i++) { + var elementSelector = toggles[i].getAttribute('data-toggle-selector'); + var element = document.querySelector(elementSelector); - /* needed because when the tab contains HTML contents, user can click */ - /* on any of those elements instead of their parent '
  • ' element */ - while (activeTab.tagName.toLowerCase() !== 'li') { - activeTab = activeTab.parentNode; - } + addClass(element, 'sf-toggle-content'); - /* get the full list of tabs through the parent of the active tab element */ - var tabNavigation = activeTab.parentNode.children; - for (var k = 0; k < tabNavigation.length; k++) { - var tabId = tabNavigation[k].getAttribute('data-tab-id'); - document.getElementById(tabId).className = 'hidden'; - removeClass(tabNavigation[k], 'active'); - } + if (toggles[i].hasAttribute('data-toggle-initial') && toggles[i].getAttribute('data-toggle-initial') == 'display') { + addClass(toggles[i], 'sf-toggle-on'); + addClass(element, 'sf-toggle-visible'); + } else { + addClass(toggles[i], 'sf-toggle-off'); + addClass(element, 'sf-toggle-hidden'); + } - addClass(activeTab, 'active'); - var activeTabId = activeTab.getAttribute('data-tab-id'); - document.getElementById(activeTabId).className = 'block'; - }); - } + addEventListener(toggles[i], 'click', function(e) { + e.preventDefault(); - tabGroups[i].setAttribute('data-processed', 'true'); + if ('' !== window.getSelection().toString()) { + /* Don't do anything on text selection */ + return; } - }, - - createToggles: function() { - var toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])'); - - for (var i = 0; i < toggles.length; i++) { - var elementSelector = toggles[i].getAttribute('data-toggle-selector'); - var element = document.querySelector(elementSelector); - - addClass(element, 'sf-toggle-content'); - - if (toggles[i].hasAttribute('data-toggle-initial') && toggles[i].getAttribute('data-toggle-initial') == 'display') { - addClass(toggles[i], 'sf-toggle-on'); - addClass(element, 'sf-toggle-visible'); - } else { - addClass(toggles[i], 'sf-toggle-off'); - addClass(element, 'sf-toggle-hidden'); - } - - addEventListener(toggles[i], 'click', function(e) { - e.preventDefault(); - if ('' !== window.getSelection().toString()) { - /* Don't do anything on text selection */ - return; - } + var toggle = e.target || e.srcElement; - var toggle = e.target || e.srcElement; + /* needed because when the toggle contains HTML contents, user can click */ + /* on any of those elements instead of their parent '.sf-toggle' element */ + while (!hasClass(toggle, 'sf-toggle')) { + toggle = toggle.parentNode; + } - /* needed because when the toggle contains HTML contents, user can click */ - /* on any of those elements instead of their parent '.sf-toggle' element */ - while (!hasClass(toggle, 'sf-toggle')) { - toggle = toggle.parentNode; - } + var element = document.querySelector(toggle.getAttribute('data-toggle-selector')); - var element = document.querySelector(toggle.getAttribute('data-toggle-selector')); + toggleClass(toggle, 'sf-toggle-on'); + toggleClass(toggle, 'sf-toggle-off'); + toggleClass(element, 'sf-toggle-hidden'); + toggleClass(element, 'sf-toggle-visible'); - toggleClass(toggle, 'sf-toggle-on'); - toggleClass(toggle, 'sf-toggle-off'); - toggleClass(element, 'sf-toggle-hidden'); - toggleClass(element, 'sf-toggle-visible'); + /* the toggle doesn't change its contents when clicking on it */ + if (!toggle.hasAttribute('data-toggle-alt-content')) { + return; + } - /* the toggle doesn't change its contents when clicking on it */ - if (!toggle.hasAttribute('data-toggle-alt-content')) { - return; - } + if (!toggle.hasAttribute('data-toggle-original-content')) { + toggle.setAttribute('data-toggle-original-content', toggle.innerHTML); + } - if (!toggle.hasAttribute('data-toggle-original-content')) { - toggle.setAttribute('data-toggle-original-content', toggle.innerHTML); - } + var currentContent = toggle.innerHTML; + var originalContent = toggle.getAttribute('data-toggle-original-content'); + var altContent = toggle.getAttribute('data-toggle-alt-content'); + toggle.innerHTML = currentContent !== altContent ? altContent : originalContent; + }); - var currentContent = toggle.innerHTML; - var originalContent = toggle.getAttribute('data-toggle-original-content'); - var altContent = toggle.getAttribute('data-toggle-alt-content'); - toggle.innerHTML = currentContent !== altContent ? altContent : originalContent; - }); + /* Prevents from disallowing clicks on links inside toggles */ + var toggleLinks = toggles[i].querySelectorAll('a'); + for (var j = 0; j < toggleLinks.length; j++) { + addEventListener(toggleLinks[j], 'click', function(e) { + e.stopPropagation(); + }); + } - /* Prevents from disallowing clicks on links inside toggles */ - var toggleLinks = toggles[i].querySelectorAll('a'); - for (var j = 0; j < toggleLinks.length; j++) { - addEventListener(toggleLinks[j], 'click', function(e) { - e.stopPropagation(); - }); - } + /* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */ + var copyToClipboardElements = toggles[i].querySelectorAll('span[data-clipboard-text]'); + for (var k = 0; k < copyToClipboardElements.length; k++) { + addEventListener(copyToClipboardElements[k], 'click', function(e) { + e.stopPropagation(); + }); + } - /* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */ - var copyToClipboardElements = toggles[i].querySelectorAll('span[data-clipboard-text]'); - for (var k = 0; k < copyToClipboardElements.length; k++) { - addEventListener(copyToClipboardElements[k], 'click', function(e) { - e.stopPropagation(); - }); - } + toggles[i].setAttribute('data-processed', 'true'); + } + })(); - toggles[i].setAttribute('data-processed', 'true'); + (function createFilters() { + document.querySelectorAll('[data-filters] [data-filter]').forEach(function (filter) { + var filters = filter.closest('[data-filters]'), + type = 'choice', + name = filter.dataset.filter, + ucName = name.charAt(0).toUpperCase()+name.slice(1), + list = document.createElement('ul'), + values = filters.dataset['filter'+ucName] || filters.querySelectorAll('[data-filter-'+name+']'), + labels = {}, + defaults = null, + indexed = {}, + processed = {}; + if (typeof values === 'string') { + type = 'level'; + labels = values.split(','); + values = values.toLowerCase().split(','); + defaults = values.length - 1; + } + addClass(list, 'filter-list'); + addClass(list, 'filter-list-'+type); + values.forEach(function (value, i) { + if (value instanceof HTMLElement) { + value = value.dataset['filter'+ucName]; } - }, - - createFilters: function() { - document.querySelectorAll('[data-filters] [data-filter]').forEach(function (filter) { - var filters = filter.closest('[data-filters]'), - type = 'choice', - name = filter.dataset.filter, - ucName = name.charAt(0).toUpperCase()+name.slice(1), - list = document.createElement('ul'), - values = filters.dataset['filter'+ucName] || filters.querySelectorAll('[data-filter-'+name+']'), - labels = {}, - defaults = null, - indexed = {}, - processed = {}; - if (typeof values === 'string') { - type = 'level'; - labels = values.split(','); - values = values.toLowerCase().split(','); - defaults = values.length - 1; - } - addClass(list, 'filter-list'); - addClass(list, 'filter-list-'+type); - values.forEach(function (value, i) { - if (value instanceof HTMLElement) { - value = value.dataset['filter'+ucName]; - } - if (value in processed) { + if (value in processed) { + return; + } + var option = document.createElement('li'), + label = i in labels ? labels[i] : value, + active = false, + matches; + if ('' === label) { + option.innerHTML = '(none)'; + } else { + option.innerText = label; + } + option.dataset.filter = value; + option.setAttribute('title', 1 === (matches = filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').length) ? 'Matches 1 row' : 'Matches '+matches+' rows'); + indexed[value] = i; + list.appendChild(option); + addEventListener(option, 'click', function () { + if ('choice' === type) { + filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { + if (option.dataset.filter === row.dataset['filter'+ucName]) { + toggleClass(row, 'filter-hidden-'+name); + } + }); + toggleClass(option, 'active'); + } else if ('level' === type) { + if (i === this.parentNode.querySelectorAll('.active').length - 1) { return; } - var option = document.createElement('li'), - label = i in labels ? labels[i] : value, - active = false, - matches; - if ('' === label) { - option.innerHTML = '(none)'; - } else { - option.innerText = label; - } - option.dataset.filter = value; - option.setAttribute('title', 1 === (matches = filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').length) ? 'Matches 1 row' : 'Matches '+matches+' rows'); - indexed[value] = i; - list.appendChild(option); - addEventListener(option, 'click', function () { - if ('choice' === type) { - filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { - if (option.dataset.filter === row.dataset['filter'+ucName]) { - toggleClass(row, 'filter-hidden-'+name); - } - }); - toggleClass(option, 'active'); - } else if ('level' === type) { - if (i === this.parentNode.querySelectorAll('.active').length - 1) { - return; + this.parentNode.querySelectorAll('li').forEach(function (currentOption, j) { + if (j <= i) { + addClass(currentOption, 'active'); + if (i === j) { + addClass(currentOption, 'last-active'); + } else { + removeClass(currentOption, 'last-active'); } - this.parentNode.querySelectorAll('li').forEach(function (currentOption, j) { - if (j <= i) { - addClass(currentOption, 'active'); - if (i === j) { - addClass(currentOption, 'last-active'); - } else { - removeClass(currentOption, 'last-active'); - } - } else { - removeClass(currentOption, 'active'); - removeClass(currentOption, 'last-active'); - } - }); - filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { - if (i < indexed[row.dataset['filter'+ucName]]) { - addClass(row, 'filter-hidden-'+name); - } else { - removeClass(row, 'filter-hidden-'+name); - } - }); + } else { + removeClass(currentOption, 'active'); + removeClass(currentOption, 'last-active'); } }); - if ('choice' === type) { - active = null === defaults || 0 <= defaults.indexOf(value); - } else if ('level' === type) { - active = i <= defaults; - if (active && i === defaults) { - addClass(option, 'last-active'); + filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { + if (i < indexed[row.dataset['filter'+ucName]]) { + addClass(row, 'filter-hidden-'+name); + } else { + removeClass(row, 'filter-hidden-'+name); } - } - if (active) { - addClass(option, 'active'); - } else { - filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').forEach(function (row) { - toggleClass(row, 'filter-hidden-'+name); - }); - } - processed[value] = true; - }); - - if (1 < list.childNodes.length) { - filter.appendChild(list); - filter.dataset.filtered = ''; + }); } }); + if ('choice' === type) { + active = null === defaults || 0 <= defaults.indexOf(value); + } else if ('level' === type) { + active = i <= defaults; + if (active && i === defaults) { + addClass(option, 'last-active'); + } + } + if (active) { + addClass(option, 'active'); + } else { + filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').forEach(function (row) { + toggleClass(row, 'filter-hidden-'+name); + }); + } + processed[value] = true; + }); + + if (1 < list.childNodes.length) { + filter.appendChild(list); + filter.dataset.filtered = ''; } - }; + }); })(); - - Sfjs.addEventListener(document, 'DOMContentLoaded', function() { - Sfjs.createTabs(); - Sfjs.createToggles(); - Sfjs.createFilters(); - }); -} +})(); /*]]>*/ diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index ccf25ad5631ed..22a88f144f7c1 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -44,7 +44,6 @@ "symfony/uid": "^5.4|^6.0" }, "conflict": { - "phpunit/phpunit": "<5.4.3", "symfony/console": "<5.4", "symfony/dependency-injection": "<5.4", "symfony/doctrine-bridge": "<5.4.21|>=6,<6.2.7", diff --git a/src/Symfony/Component/HttpClient/HttplugClient.php b/src/Symfony/Component/HttpClient/HttplugClient.php index 4d539219174be..661f9fec0b897 100644 --- a/src/Symfony/Component/HttpClient/HttplugClient.php +++ b/src/Symfony/Component/HttpClient/HttplugClient.php @@ -46,7 +46,7 @@ } if (!interface_exists(RequestFactory::class)) { - throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require nyholm/psr7".'); + throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory".'); } if (!interface_exists(RequestFactoryInterface::class)) { diff --git a/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php b/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php index 27745e46d881f..85d7e01d6c96f 100644 --- a/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php +++ b/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php @@ -120,7 +120,11 @@ public function createPsr7Response(ResponseInterface $response, bool $buffer = f foreach ($response->getHeaders(false) as $name => $values) { foreach ($values as $value) { - $psrResponse = $psrResponse->withAddedHeader($name, $value); + try { + $psrResponse = $psrResponse->withAddedHeader($name, $value); + } catch (\InvalidArgumentException $e) { + // ignore invalid header + } } } diff --git a/src/Symfony/Component/HttpClient/Response/AmpResponse.php b/src/Symfony/Component/HttpClient/Response/AmpResponse.php index d46e4036d801c..9aad826417360 100644 --- a/src/Symfony/Component/HttpClient/Response/AmpResponse.php +++ b/src/Symfony/Component/HttpClient/Response/AmpResponse.php @@ -47,7 +47,6 @@ final class AmpResponse implements ResponseInterface, StreamableInterface private AmpClientState $multi; private ?array $options; - private CancellationTokenSource $canceller; private \Closure $onProgress; private static ?string $delay = null; @@ -73,7 +72,7 @@ public function __construct(AmpClientState $multi, Request $request, array $opti $info = &$this->info; $headers = &$this->headers; - $canceller = $this->canceller = new CancellationTokenSource(); + $canceller = new CancellationTokenSource(); $handle = &$this->handle; $info['url'] = (string) $request->getUri(); @@ -349,7 +348,7 @@ private static function followRedirects(Request $originRequest, AmpClientState $ } foreach ($originRequest->getRawHeaders() as [$name, $value]) { - $request->setHeader($name, $value); + $request->addHeader($name, $value); } if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) { diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index bfea82b6ffa5c..9c151546a4472 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -79,17 +79,7 @@ public function __construct(CurlClientState $multi, \CurlHandle|string $ch, arra } curl_setopt($ch, \CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int { - if (0 !== substr_compare($data, "\r\n", -2)) { - return 0; - } - - $len = 0; - - foreach (explode("\r\n", substr($data, 0, -2)) as $data) { - $len += 2 + self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); - } - - return $len; + return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); }); if (null === $options) { @@ -366,19 +356,29 @@ private static function select(ClientState $multi, float $timeout): int */ private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int { + if (!str_ends_with($data, "\r\n")) { + return 0; + } + $waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0'; if ('H' !== $waitFor[0]) { return \strlen($data); // Ignore HTTP trailers } - if ('' !== $data) { + $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE); + + if ($statusCode !== $info['http_code'] && !preg_match("#^HTTP/\d+(?:\.\d+)? {$statusCode}(?: |\r\n$)#", $data)) { + return \strlen($data); // Ignore headers from responses to CONNECT requests + } + + if ("\r\n" !== $data) { // Regular header line: add it to the list - self::addResponseHeaders([$data], $info, $headers); + self::addResponseHeaders([substr($data, 0, -2)], $info, $headers); if (!str_starts_with($data, 'HTTP/')) { if (0 === stripos($data, 'Location:')) { - $location = trim(substr($data, 9)); + $location = trim(substr($data, 9, -2)); } return \strlen($data); @@ -401,7 +401,7 @@ private static function parseHeaderLine($ch, string $data, array &$info, array & // End of headers: handle informational responses, redirects, etc. - if (200 > $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE)) { + if (200 > $statusCode) { $multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers); $location = null; diff --git a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php index 23dc7b6d9f0b0..8bec22a1ebbad 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php @@ -267,4 +267,22 @@ function (\Exception $exception) use ($errorMessage, &$failureCallableCalled, $c $this->assertSame(200, $response->getStatusCode()); $this->assertSame('OK', (string) $response->getBody()); } + + public function testInvalidHeaderResponse() + { + $responseHeaders = [ + // space in header name not allowed in RFC 7230 + ' X-XSS-Protection' => '0', + 'Cache-Control' => 'no-cache', + ]; + $response = new MockResponse('body', ['response_headers' => $responseHeaders]); + $this->assertArrayHasKey(' x-xss-protection', $response->getHeaders()); + + $client = new HttplugClient(new MockHttpClient($response)); + $request = $client->createRequest('POST', 'http://localhost:8057/post') + ->withBody($client->createStream('foo=0123456789')); + + $resultResponse = $client->sendRequest($request); + $this->assertCount(1, $resultResponse->getHeaders()); + } } diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json index 1e9adde053093..34a1714bf3dd1 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -36,6 +36,7 @@ "guzzlehttp/promises": "^1.4", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", + "php-http/message-factory": "^1.0", "psr/http-client": "^1.0", "symfony/dependency-injection": "^5.4|^6.0", "symfony/http-kernel": "^5.4|^6.0", diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index 72cef7c0ea114..749be5b6c1525 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -319,7 +319,7 @@ public function sendContent(): static while ('' !== $data) { $read = fwrite($out, $data); if (false === $read || connection_aborted()) { - break; + break 2; } if (0 < $length) { $length -= $read; diff --git a/src/Symfony/Component/HttpFoundation/HeaderBag.php b/src/Symfony/Component/HttpFoundation/HeaderBag.php index 0883024b3b50b..dc5a9a60a2e48 100644 --- a/src/Symfony/Component/HttpFoundation/HeaderBag.php +++ b/src/Symfony/Component/HttpFoundation/HeaderBag.php @@ -63,7 +63,7 @@ public function __toString(): string * * @param string|null $key The name of the headers to return or null to get them all * - * @return array>|array + * @return array>|list */ public function all(string $key = null): array { diff --git a/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php b/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php index 168f69efe848f..02f6c64cfce38 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\UrlHelper; use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; class UrlHelperTest extends TestCase { @@ -64,11 +65,44 @@ public function testGenerateAbsoluteUrlWithRequestContext($path, $baseUrl, $host } $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path); + $helper = new UrlHelper(new RequestStack(), $requestContext); $this->assertEquals($expected, $helper->getAbsoluteUrl($path)); } + /** + * @dataProvider getGenerateAbsoluteUrlRequestContextData + */ + public function testGenerateAbsoluteUrlWithRequestContextAwareInterface($path, $baseUrl, $host, $scheme, $httpPort, $httpsPort, $expected) + { + if (!class_exists(RequestContext::class)) { + $this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.'); + } + + $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path); + $contextAware = new class($requestContext) implements RequestContextAwareInterface { + public function __construct( + private RequestContext $requestContext, + ) { + } + + public function setContext(RequestContext $context): void + { + $this->requestContext = $context; + } + + public function getContext(): RequestContext + { + return $this->requestContext; + } + }; + + $helper = new UrlHelper(new RequestStack(), $contextAware); + + $this->assertEquals($expected, $helper->getAbsoluteUrl($path)); + } + /** * @dataProvider getGenerateAbsoluteUrlRequestContextData */ diff --git a/src/Symfony/Component/HttpFoundation/UrlHelper.php b/src/Symfony/Component/HttpFoundation/UrlHelper.php index 42fcf0459ab8a..d5641eff86d58 100644 --- a/src/Symfony/Component/HttpFoundation/UrlHelper.php +++ b/src/Symfony/Component/HttpFoundation/UrlHelper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation; use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; /** * A helper service for manipulating URLs within and outside the request scope. @@ -20,13 +21,11 @@ */ final class UrlHelper { - private RequestStack $requestStack; - private ?RequestContext $requestContext; - public function __construct(RequestStack $requestStack, RequestContext $requestContext = null) - { - $this->requestStack = $requestStack; - $this->requestContext = $requestContext; + public function __construct( + private RequestStack $requestStack, + private RequestContextAwareInterface|RequestContext|null $requestContext = null, + ) { } public function getAbsoluteUrl(string $path): string @@ -73,28 +72,36 @@ public function getRelativePath(string $path): string private function getAbsoluteUrlFromContext(string $path): string { - if (null === $this->requestContext || '' === $host = $this->requestContext->getHost()) { + if (null === $context = $this->requestContext) { + return $path; + } + + if ($context instanceof RequestContextAwareInterface) { + $context = $context->getContext(); + } + + if ('' === $host = $context->getHost()) { return $path; } - $scheme = $this->requestContext->getScheme(); + $scheme = $context->getScheme(); $port = ''; - if ('http' === $scheme && 80 !== $this->requestContext->getHttpPort()) { - $port = ':'.$this->requestContext->getHttpPort(); - } elseif ('https' === $scheme && 443 !== $this->requestContext->getHttpsPort()) { - $port = ':'.$this->requestContext->getHttpsPort(); + if ('http' === $scheme && 80 !== $context->getHttpPort()) { + $port = ':'.$context->getHttpPort(); + } elseif ('https' === $scheme && 443 !== $context->getHttpsPort()) { + $port = ':'.$context->getHttpsPort(); } if ('#' === $path[0]) { - $queryString = $this->requestContext->getQueryString(); - $path = $this->requestContext->getPathInfo().($queryString ? '?'.$queryString : '').$path; + $queryString = $context->getQueryString(); + $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path; } elseif ('?' === $path[0]) { - $path = $this->requestContext->getPathInfo().$path; + $path = $context->getPathInfo().$path; } if ('/' !== $path[0]) { - $path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path; + $path = rtrim($context->getBaseUrl(), '/').'/'.$path; } return $scheme.'://'.$host.$port.$path; diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index 52ac242141af6..049902585a734 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -78,7 +78,7 @@ public function getArguments(Request $request, callable $controller, \Reflection $representative = get_debug_type($representative); } - throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.', $representative, $metadata->getName())); + throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or there is a non-optional argument after this one.', $representative, $metadata->getName())); } return $arguments; diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php index e7be3b88e7a34..799679effc0fc 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\DependencyInjection; +use ProxyManager\Proxy\LazyLoadingInterface; +use Symfony\Component\VarExporter\LazyObjectInterface; use Symfony\Contracts\Service\ResetInterface; /** @@ -39,6 +41,14 @@ public function __construct(\Traversable $resettableServices, array $resetMethod public function reset() { foreach ($this->resettableServices as $id => $service) { + if ($service instanceof LazyObjectInterface && !$service->isLazyObjectInitialized(true)) { + continue; + } + + if ($service instanceof LazyLoadingInterface && !$service->isProxyInitialized()) { + continue; + } + foreach ((array) $this->resetMethods[$id] as $resetMethod) { if ('?' === $resetMethod[0] && !method_exists($service, $resetMethod = substr($resetMethod, 1))) { continue; diff --git a/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php b/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php index ca01a9240b373..a7882f531d606 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php @@ -119,4 +119,15 @@ protected function removeFromControl(Response $response) $response->headers->set('Surrogate-Control', preg_replace(sprintf('#content="%s/1.0",\s*#', $upperName), '', $value)); } } + + protected static function generateBodyEvalBoundary(): string + { + static $cookie; + $cookie = hash('xxh128', $cookie ?? $cookie = random_bytes(16), true); + $boundary = base64_encode($cookie); + + \assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === \strlen($boundary)); + + return $boundary; + } } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php index 9e7c3f898ab83..08e0e899a1ba3 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php @@ -71,8 +71,8 @@ public function process(Request $request, Response $response): Response $content = preg_replace('#.*?#s', '', $content); $content = preg_replace('#]+>#s', '', $content); + $boundary = self::generateBodyEvalBoundary(); $chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); - $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]); $i = 1; while (isset($chunks[$i])) { @@ -86,16 +86,10 @@ public function process(Request $request, Response $response): Response throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.'); } - $chunks[$i] = sprintf('surrogate->handle($this, %s, %s, %s) ?>'."\n", - var_export($options['src'], true), - var_export($options['alt'] ?? '', true), - isset($options['onerror']) && 'continue' === $options['onerror'] ? 'true' : 'false' - ); - ++$i; - $chunks[$i] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[$i]); - ++$i; + $chunks[$i] = $boundary.$options['src']."\n".($options['alt'] ?? '')."\n".('continue' === ($options['onerror'] ?? ''))."\n"; + $i += 2; } - $content = implode('', $chunks); + $content = $boundary.implode('', $chunks).$boundary; $response->setContent($content); $response->headers->set('X-Body-Eval', 'ESI'); diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index f77a99e0ffa83..fb84d96428f42 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -29,6 +29,8 @@ */ class HttpCache implements HttpKernelInterface, TerminableInterface { + public const BODY_EVAL_BOUNDARY_LENGTH = 24; + private HttpKernelInterface $kernel; private StoreInterface $store; private Request $request; @@ -614,12 +616,22 @@ protected function store(Request $request, Response $response) private function restoreResponseBody(Request $request, Response $response) { if ($response->headers->has('X-Body-Eval')) { + \assert(self::BODY_EVAL_BOUNDARY_LENGTH === 24); + ob_start(); - if ($response->headers->has('X-Body-File')) { - include $response->headers->get('X-Body-File'); - } else { - eval('; ?>'.$response->getContent().'getContent(); + $boundary = substr($content, 0, 24); + $j = strpos($content, $boundary, 24); + echo substr($content, 24, $j - 24); + $i = $j + 24; + + while (false !== $j = strpos($content, $boundary, $i)) { + [$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4); + $i = $j + 24; + + echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors); + echo $part; } $response->setContent(ob_get_clean()); diff --git a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php index 0c9f10823a5c4..05b5f5d7d60d0 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php @@ -148,7 +148,7 @@ public function update(Response $response) if (is_numeric($this->ageDirectives['expires'])) { $date = clone $response->getDate(); - $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds'); + $date = $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds'); $response->setExpires($date); } } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php index 4bd1cef0a7885..4ca5cc6bfacd1 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php @@ -52,9 +52,8 @@ public function process(Request $request, Response $response): Response // we don't use a proper XML parser here as we can have SSI tags in a plain text response $content = $response->getContent(); - + $boundary = self::generateBodyEvalBoundary(); $chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); - $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]); $i = 1; while (isset($chunks[$i])) { @@ -68,14 +67,10 @@ public function process(Request $request, Response $response): Response throw new \RuntimeException('Unable to process an SSI tag without a "virtual" attribute.'); } - $chunks[$i] = sprintf('surrogate->handle($this, %s, \'\', false) ?>'."\n", - var_export($options['virtual'], true) - ); - ++$i; - $chunks[$i] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[$i]); - ++$i; + $chunks[$i] = $boundary.$options['virtual']."\n\n\n"; + $i += 2; } - $content = implode('', $chunks); + $content = $boundary.implode('', $chunks).$boundary; $response->setContent($content); $response->headers->set('X-Body-Eval', 'SSI'); diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Store.php b/src/Symfony/Component/HttpKernel/HttpCache/Store.php index b21cb45874ea7..f4a855e177864 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php @@ -467,15 +467,25 @@ private function persistResponse(Response $response): array /** * Restores a Response from the HTTP headers and body. */ - private function restoreResponse(array $headers, string $path = null): Response + private function restoreResponse(array $headers, string $path = null): ?Response { $status = $headers['X-Status'][0]; unset($headers['X-Status']); + $content = null; if (null !== $path) { $headers['X-Body-File'] = [$path]; + unset($headers['x-body-file']); + + if ($headers['X-Body-Eval'] ?? $headers['x-body-eval'] ?? false) { + $content = file_get_contents($path); + \assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === 24); + if (48 > \strlen($content) || substr($content, -24) !== substr($content, 0, 24)) { + return null; + } + } } - return new Response($path, $status, $headers); + return new Response($content, $status, $headers); } } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index e9694e978e3c5..1ad714e6d98a9 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -75,11 +75,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.2.10'; - public const VERSION_ID = 60210; + public const VERSION = '6.2.11'; + public const VERSION_ID = 60211; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 2; - public const RELEASE_VERSION = 10; + public const RELEASE_VERSION = 11; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2023'; diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php index 604d2b0d13b82..3390dcc1e4d64 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php @@ -14,8 +14,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService; +use Symfony\Component\HttpKernel\Tests\Fixtures\LazyResettableService; use Symfony\Component\HttpKernel\Tests\Fixtures\MultiResettableService; use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService; +use Symfony\Component\VarExporter\ProxyHelper; class ServicesResetterTest extends TestCase { @@ -46,4 +48,29 @@ public function testResetServices() $this->assertSame(1, MultiResettableService::$resetFirstCounter); $this->assertSame(1, MultiResettableService::$resetSecondCounter); } + + public function testResetLazyServices() + { + $proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(LazyResettableService::class)); + eval('class LazyResettableServiceProxy'.$proxyCode); + + $lazyService = \LazyResettableServiceProxy::createLazyProxy(fn (): LazyResettableService => new LazyResettableService()); + + $resetter = new ServicesResetter(new \ArrayIterator([ + 'lazy' => $lazyService, + ]), [ + 'lazy' => ['reset'], + ]); + + $resetter->reset(); + $this->assertSame(0, LazyResettableService::$counter); + + $resetter->reset(); + $this->assertSame(0, LazyResettableService::$counter); + + $this->assertTrue($lazyService->foo()); + + $resetter->reset(); + $this->assertSame(1, LazyResettableService::$counter); + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php index 78c8333af6a3d..90d9aa6d33544 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php @@ -149,7 +149,7 @@ public function testAttributeConfigurationsAreSetOnResponse() $this->assertSame('5', $this->response->headers->getCacheControlDirective('max-stale')); $this->assertSame('6', $this->response->headers->getCacheControlDirective('stale-while-revalidate')); $this->assertSame('7', $this->response->headers->getCacheControlDirective('stale-if-error')); - $this->assertInstanceOf(\DateTime::class, $this->response->getExpires()); + $this->assertInstanceOf(\DateTimeInterface::class, $this->response->getExpires()); } public function testCacheMaxAgeSupportsStrtotimeFormat() diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/LazyResettableService.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/LazyResettableService.php new file mode 100644 index 0000000000000..543cf0d9538d3 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/LazyResettableService.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Fixtures; + +class LazyResettableService +{ + public static $counter = 0; + + public function foo(): bool + { + return true; + } + + public function reset(): void + { + ++self::$counter; + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php index 290bd94bdcb97..e876f28189087 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php @@ -102,7 +102,7 @@ public function testMultilineEsiRemoveTagsAreRemoved() $response = new Response(' Keep this'." And this"); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals(' Keep this And this', $response->getContent()); + $this->assertEquals(' Keep this And this', substr($response->getContent(), 24, -24)); } public function testCommentTagsAreRemoved() @@ -113,7 +113,7 @@ public function testCommentTagsAreRemoved() $response = new Response(' Keep this'); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals(' Keep this', $response->getContent()); + $this->assertEquals(' Keep this', substr($response->getContent(), 24, -24)); } public function testProcess() @@ -124,23 +124,27 @@ public function testProcess() $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals('foo surrogate->handle($this, \'...\', \'alt\', true) ?>'."\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "...\nalt\n1\n", ''], $content); $this->assertEquals('ESI', $response->headers->get('x-body-eval')); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals('foo surrogate->handle($this, \'foo\\\'\', \'bar\\\'\', true) ?>'."\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "foo'\nbar'\n1\n", ''], $content); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content); } public function testProcessEscapesPhpTags() @@ -151,7 +155,8 @@ public function testProcessEscapesPhpTags() $response = new Response(''); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals('php cript language=php>', $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', '', ''], $content); } public function testProcessWhenNoSrcInAnEsi() diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php index bce749e85cc91..0cf32f6020f3c 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php @@ -19,7 +19,7 @@ use Symfony\Component\HttpKernel\HttpCache\Store; use Symfony\Component\HttpKernel\HttpKernelInterface; -class HttpCacheTestCase extends TestCase +abstract class HttpCacheTestCase extends TestCase { protected $kernel; protected $cache; diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php index 72c12c75f2e77..fefa64572a60c 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php @@ -275,7 +275,7 @@ public function testCacheControlMerging(array $expects, array $master, array $su case 'expires': $expires = clone $response->getDate(); - $expires->modify('+'.$value.' seconds'); + $expires = $expires->modify('+'.$value.' seconds'); $response->setExpires($expires); break; diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php index a1f1f1593d3f3..97cc8fccd03d0 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php @@ -101,13 +101,15 @@ public function testProcess() $response = new Response('foo '); $ssi->process($request, $response); - $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content); $this->assertEquals('SSI', $response->headers->get('x-body-eval')); $response = new Response('foo '); $ssi->process($request, $response); - $this->assertEquals("foo surrogate->handle(\$this, 'foo\\'', '', false) ?>\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "foo'\n\n\n", ''], $content); } public function testProcessEscapesPhpTags() @@ -118,7 +120,8 @@ public function testProcessEscapesPhpTags() $response = new Response(''); $ssi->process($request, $response); - $this->assertEquals('php cript language=php>', $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', '', ''], $content); } public function testProcessWhenNoSrcInAnSsi() diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php index 014009e185bdd..dccadc01985d4 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php @@ -200,7 +200,7 @@ public function testRestoresResponseContentFromEntityStoreWithLookup() { $this->storeSimpleEntry(); $response = $this->store->lookup($this->request); - $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test')), $response->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test')), $response->headers->get('X-Body-File')); } public function testInvalidatesMetaAndEntityStoreEntriesWithInvalidate() @@ -253,9 +253,9 @@ public function testStoresMultipleResponsesForEachVaryCombination() $res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']); $this->store->write($req3, $res3); - $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 3')), $this->store->lookup($req3)->getContent()); - $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 2')), $this->store->lookup($req2)->getContent()); - $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 1')), $this->store->lookup($req1)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File')); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File')); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File')); $this->assertCount(3, $this->getStoreMetadata($key)); } @@ -265,17 +265,17 @@ public function testOverwritesNonVaryingResponseWithStore() $req1 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $res1 = new Response('test 1', 200, ['Vary' => 'Foo Bar']); $this->store->write($req1, $res1); - $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 1')), $this->store->lookup($req1)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File')); $req2 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam']); $res2 = new Response('test 2', 200, ['Vary' => 'Foo Bar']); $this->store->write($req2, $res2); - $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 2')), $this->store->lookup($req2)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File')); $req3 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']); $key = $this->store->write($req3, $res3); - $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 3')), $this->store->lookup($req3)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File')); $this->assertCount(2, $this->getStoreMetadata($key)); } @@ -330,6 +330,33 @@ public function testDoesNotStorePrivateHeaders() $this->assertNotEmpty($response->headers->getCookies()); } + public function testDiscardsInvalidBodyEval() + { + $request = Request::create('https://example.com/foo'); + $response = new Response('foo', 200, ['X-Body-Eval' => 'SSI']); + + $this->store->write($request, $response); + $this->assertNull($this->store->lookup($request)); + + $request = Request::create('https://example.com/foo'); + $content = str_repeat('a', 24).'b'.str_repeat('a', 24).'b'; + $response = new Response($content, 200, ['X-Body-Eval' => 'SSI']); + + $this->store->write($request, $response); + $this->assertNull($this->store->lookup($request)); + } + + public function testLoadsBodyEval() + { + $request = Request::create('https://example.com/foo'); + $content = str_repeat('a', 24).'b'.str_repeat('a', 24); + $response = new Response($content, 200, ['X-Body-Eval' => 'SSI']); + + $this->store->write($request, $response); + $response = $this->store->lookup($request); + $this->assertSame($content, $response->getContent()); + } + protected function storeSimpleEntry($path = null, $headers = []) { $path ??= '/test'; diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index b6fb8c2d1fb5a..9ee322035cd32 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -40,6 +40,7 @@ "symfony/translation": "^5.4|^6.0", "symfony/translation-contracts": "^1.1|^2|^3", "symfony/uid": "^5.4|^6.0", + "symfony/var-exporter": "^6.2", "psr/cache": "^1.0|^2.0|^3.0", "twig/twig": "^2.13|^3.0.4" }, diff --git a/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php b/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php index c25c8b2c974dc..c0acf6f6c3d3b 100644 --- a/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php +++ b/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php @@ -16,7 +16,7 @@ * * @author Alireza Mirsepassi */ -#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class AsMessageHandler { public function __construct( diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index 1763fe245a0e8..a25930bbd33f9 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -131,7 +131,7 @@ private function registerHandlers(ContainerBuilder $container, array $busIds) } if ('__invoke' !== $method) { - $wrapperDefinition = (new Definition('callable'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable'); + $wrapperDefinition = (new Definition('Closure'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable'); $definitions[$definitionId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($message.':'.$priority.':'.$serviceId.':'.$method)] = $wrapperDefinition; } else { diff --git a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php index 0f071e82e702d..0af2673d1e13d 100644 --- a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php +++ b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php @@ -117,7 +117,8 @@ public static function getSubscribedEvents(): array private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInterface $retryStrategy): bool { - if ($e instanceof RecoverableExceptionInterface) { + $isRetryable = $retryStrategy->isRetryable($envelope, $e); + if ($isRetryable && $e instanceof RecoverableExceptionInterface) { return true; } @@ -126,7 +127,7 @@ private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInt if ($e instanceof HandlerFailedException) { $shouldNotRetry = true; foreach ($e->getNestedExceptions() as $nestedException) { - if ($nestedException instanceof RecoverableExceptionInterface) { + if ($isRetryable && $nestedException instanceof RecoverableExceptionInterface) { return true; } @@ -144,7 +145,7 @@ private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInt return false; } - return $retryStrategy->isRetryable($envelope, $e); + return $isRetryable; } private function getRetryStrategyForTransport(string $alias): ?RetryStrategyInterface diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index a72efcc15e29e..cffeac40ad22d 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -360,7 +360,7 @@ public function testGetClassesAndMethodsAndPrioritiesFromTheSubscriber() $dummyHandlerReference = $dummyHandlerDescriptorDefinition->getArgument(0); $dummyHandlerDefinition = $container->getDefinition($dummyHandlerReference); - $this->assertSame('callable', $dummyHandlerDefinition->getClass()); + $this->assertSame('Closure', $dummyHandlerDefinition->getClass()); $this->assertEquals([new Reference(HandlerMappingMethods::class), 'dummyMethod'], $dummyHandlerDefinition->getArgument(0)); $this->assertSame(['Closure', 'fromCallable'], $dummyHandlerDefinition->getFactory()); diff --git a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php index fccdae9bfa215..cb79f539dc325 100644 --- a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php +++ b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php @@ -63,7 +63,7 @@ public function testRecoverableStrategyCausesRetry() $senderLocator->expects($this->once())->method('has')->willReturn(true); $senderLocator->expects($this->once())->method('get')->willReturn($sender); $retryStategy = $this->createMock(RetryStrategyInterface::class); - $retryStategy->expects($this->never())->method('isRetryable'); + $retryStategy->expects($this->once())->method('isRetryable')->willReturn(true); $retryStategy->expects($this->once())->method('getWaitingTime')->willReturn(1000); $retryStrategyLocator = $this->createMock(ContainerInterface::class); $retryStrategyLocator->expects($this->once())->method('has')->willReturn(true); @@ -78,6 +78,27 @@ public function testRecoverableStrategyCausesRetry() $listener->onMessageFailed($event); } + public function testRetryIsOnlyAllowedWhenPermittedByRetryStrategy() + { + $senderLocator = $this->createMock(ContainerInterface::class); + $senderLocator->expects($this->never())->method('has'); + $senderLocator->expects($this->never())->method('get'); + $retryStrategy = $this->createMock(RetryStrategyInterface::class); + $retryStrategy->expects($this->once())->method('isRetryable')->willReturn(false); + $retryStrategy->expects($this->never())->method('getWaitingTime'); + $retryStrategyLocator = $this->createMock(ContainerInterface::class); + $retryStrategyLocator->expects($this->once())->method('has')->willReturn(true); + $retryStrategyLocator->expects($this->once())->method('get')->willReturn($retryStrategy); + + $listener = new SendFailedMessageForRetryListener($senderLocator, $retryStrategyLocator); + + $exception = new RecoverableMessageHandlingException('retry'); + $envelope = new Envelope(new \stdClass()); + $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception); + + $listener->onMessageFailed($event); + } + public function testEnvelopeIsSentToTransportOnRetry() { $exception = new \Exception('no!'); diff --git a/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php b/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php index ce2f2e3d211d6..c06855c3146dc 100644 --- a/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php +++ b/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php @@ -51,7 +51,7 @@ public function recipient(string $topic): static } /** - * @see PublishInput::$Subject + * @see PublishInput::$subject * * @return $this */ @@ -63,7 +63,7 @@ public function subject(string $subject): static } /** - * @see PublishInput::$MessageStructure + * @see PublishInput::$messageStructure * * @return $this */ diff --git a/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md b/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md index 3dbcbb1546247..db4759327f502 100644 --- a/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md +++ b/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md @@ -1,7 +1,7 @@ Amazon Notifier =============== -Provides [Amazon SNS](https://aws.amazon.com/de/sns/) integration for Symfony Notifier. +Provides [Amazon SNS](https://aws.amazon.com/en/sns/) integration for Symfony Notifier. DSN example ----------- @@ -10,6 +10,30 @@ DSN example AMAZON_SNS_DSN=sns://ACCESS_ID:ACCESS_KEY@default?region=REGION ``` +Adding Options to a Chat Message +-------------------------------- + +With an Amazon SNS Chat Message, you can use the `AmazonSnsOptions` class to add +message options. + +```php +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Bridge\AmazonSns\AmazonSnsOptions; + +$chatMessage = new ChatMessage('Contribute To Symfony'); + +$options = (new AmazonSnsOptions('topic_arn')) + ->subject('subject') + ->messageStructure('json') + // ... + ; + +// Add the custom options to the chat message and send the message +$chatMessage->options($options); + +$chatter->send($chatMessage); +``` + Resources --------- diff --git a/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md b/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md index 2d9b0b6d1488d..d9760759c9f90 100644 --- a/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md +++ b/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md @@ -16,6 +16,29 @@ where: - `FROM` is the sender - `TYPE_QUALITY` is the quality of your message: `N` for high, `L` for medium, `LL` for low (default: `L`) +Adding Options to a Message +--------------------------- + +With a Mobyt Message, you can use the `MobytOptions` class to add +[message options](https://gatewayapi.com/docs/apis/rest/). + +```php +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Bridge\Mobyt\MobytOptions; + +$sms = new SmsMessage('+1411111111', 'My message'); + +$options = (new MobytOptions()) + ->messageType(MobytOptions::MESSAGE_TYPE_QUALITY_HIGH) + // ... + ; + +// Add the custom options to the sms message and send the message +$sms->options($options); + +$texter->send($sms); +``` + Resources --------- diff --git a/src/Symfony/Component/Notifier/Bridge/TurboSms/Tests/TurboSmsTransportTest.php b/src/Symfony/Component/Notifier/Bridge/TurboSms/Tests/TurboSmsTransportTest.php index 386a69bdaeaa8..136ee2feb433c 100644 --- a/src/Symfony/Component/Notifier/Bridge/TurboSms/Tests/TurboSmsTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/TurboSms/Tests/TurboSmsTransportTest.php @@ -71,7 +71,16 @@ public function testSuccessfulSend() ])) ; - $client = new MockHttpClient(static function () use ($response): ResponseInterface { + $client = new MockHttpClient(static function (string $method, string $url, array $options) use ($response): ResponseInterface { + $body = json_decode($options['body'], true); + self::assertSame([ + 'sms' => [ + 'sender' => 'sender', + 'recipients' => ['380931234567'], + 'text' => 'Тест/Test', + ], + ], $body); + return $response; }); diff --git a/src/Symfony/Component/Notifier/Bridge/TurboSms/TurboSmsTransport.php b/src/Symfony/Component/Notifier/Bridge/TurboSms/TurboSmsTransport.php index 1d885abd2d07d..089e84a71b70e 100644 --- a/src/Symfony/Component/Notifier/Bridge/TurboSms/TurboSmsTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/TurboSms/TurboSmsTransport.php @@ -66,7 +66,8 @@ protected function doSend(MessageInterface $message): SentMessage $this->assertValidSubject($message->getSubject()); $fromMessage = $message->getFrom(); - if (null !== $fromMessage) { + + if ($fromMessage) { $this->assertValidFrom($fromMessage); $from = $fromMessage; } else { diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 63a402a55fc75..4436c6a16e996 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -421,7 +421,7 @@ public function wait(callable $callback = null): int do { $this->checkTimeout(); - $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen()); $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); } while ($running); diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index ad44373c1b650..4c4ebdd25e21e 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -1536,6 +1536,16 @@ public function testEnvCaseInsensitiveOnWindows() } } + public function testNotTerminableInputPipe() + { + $process = $this->getProcess('echo foo'); + $process->setInput(\STDIN); + $process->start(); + $process->setTimeout(2); + $process->wait(); + $this->assertFalse($process->isRunning()); + } + /** * @param string|array $commandline * @param mixed $input diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 990d77b7a5ed0..83f9a225a1569 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -301,7 +301,7 @@ private function readPropertiesUntil(array $zval, PropertyPathInterface $propert if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) || (\is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !\array_key_exists($property, $zval[self::VALUE])) ) { - if (!$ignoreInvalidIndices) { + if (!$ignoreInvalidIndices && !$isNullSafe) { if (!\is_array($zval[self::VALUE])) { if (!$zval[self::VALUE] instanceof \Traversable) { throw new NoSuchIndexException(sprintf('Cannot read index "%s" while trying to traverse path "%s".', $property, (string) $propertyPath)); diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index c5ecd79be1bbf..ae773e91d87ec 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -573,12 +573,28 @@ public static function getValidReadPropertyPaths(): iterable yield [(object) ['foo' => null], 'foo?.bar.baz', null]; yield [(object) ['foo' => (object) ['bar' => null]], 'foo?.bar?.baz', null]; yield [(object) ['foo' => (object) ['bar' => null]], 'foo.bar?.baz', null]; + + yield from self::getNullSafeIndexPaths(); + } + + public static function getNullSafeIndexPaths(): iterable + { yield [(object) ['foo' => ['bar' => null]], 'foo[bar?].baz', null]; yield [[], '[foo?]', null]; yield [['foo' => ['firstName' => 'Bernhard']], '[foo][bar?]', null]; yield [['foo' => ['firstName' => 'Bernhard']], '[foo][bar?][baz?]', null]; } + /** + * @dataProvider getNullSafeIndexPaths + */ + public function testNullSafeIndexWithThrowOnInvalidIndex($objectOrArray, $path, $value) + { + $this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_INDEX | PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH); + + $this->assertSame($value, $this->propertyAccessor->getValue($objectOrArray, $path)); + } + public function testTicket5755() { $object = new Ticket5775Object(); diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php index 47b3e9497287b..e8fa2077a8fe8 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php @@ -179,7 +179,10 @@ private function getDocBlockFromConstructor(string $class, string $property): ?P return null; } - $rawDocNode = $reflectionConstructor->getDocComment(); + if (!$rawDocNode = $reflectionConstructor->getDocComment()) { + return null; + } + $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode)); $phpDocNode = $this->phpDocParser->parse($tokens); $tokens->consumeTokenType(Lexer::TOKEN_END); diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 6985e2d55e939..1ef1562f9d8e5 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -142,7 +142,7 @@ public function testExtractCollection($property, array $type = null, $shortDescr public static function provideCollectionTypes() { return [ - ['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, null, new Type(Type::BUILTIN_TYPE_STRING))], null, null], + ['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)], new Type(Type::BUILTIN_TYPE_STRING))], null, null], ['iteratorCollectionWithKey', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], null, null], [ 'nestedIterators', @@ -238,6 +238,8 @@ public static function typesWithCustomPrefixesProvider() ['i', [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT, true)], null, null], ['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')], null, null], ['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))], null, null], + ['nonNullableCollectionOfNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, true))], null, null], + ['nullableCollectionOfMultipleNonNullableElementTypes', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)])], null, null], ['donotexist', null, null, null], ['staticGetter', null, null, null], ['staticSetter', null, null, null], diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 99a5e3d0a4dc4..f3405d0409ae3 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; +use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; @@ -355,6 +356,14 @@ public function testExtractConstructorTypes($property, array $type = null) $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } + /** + * @dataProvider constructorTypesProvider + */ + public function testExtractConstructorTypesReturnNullOnEmptyDocBlock($property) + { + $this->assertNull($this->extractor->getTypesFromConstructor(ConstructorDummyWithoutDocBlock::class, $property)); + } + public static function constructorTypesProvider() { return [ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 2bcb1963ff4ec..1f3ba505ddcfb 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -59,6 +59,8 @@ public function testGetProperties() 'i', 'j', 'nullableCollectionOfNonNullableElements', + 'nonNullableCollectionOfNullableElements', + 'nullableCollectionOfMultipleNonNullableElementTypes', 'emptyVar', 'iteratorCollection', 'iteratorCollectionWithKey', @@ -121,6 +123,8 @@ public function testGetPropertiesWithCustomPrefixes() 'i', 'j', 'nullableCollectionOfNonNullableElements', + 'nonNullableCollectionOfNullableElements', + 'nullableCollectionOfMultipleNonNullableElementTypes', 'emptyVar', 'iteratorCollection', 'iteratorCollectionWithKey', @@ -172,6 +176,8 @@ public function testGetPropertiesWithNoPrefixes() 'i', 'j', 'nullableCollectionOfNonNullableElements', + 'nonNullableCollectionOfNullableElements', + 'nullableCollectionOfMultipleNonNullableElementTypes', 'emptyVar', 'iteratorCollection', 'iteratorCollectionWithKey', diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithoutDocBlock.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithoutDocBlock.php new file mode 100644 index 0000000000000..7e2087d467f7d --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithoutDocBlock.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\PropertyInfo\Tests\Fixtures; + +class ConstructorDummyWithoutDocBlock +{ + public function __construct(\DateTimeZone $timezone, $date, $dateObject, \DateTimeImmutable $dateTime, $mixed) + { + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index 6682c8440f0d7..5b38fc1bcc722 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -98,6 +98,16 @@ class Dummy extends ParentDummy */ public $nullableCollectionOfNonNullableElements; + /** + * @var array + */ + public $nonNullableCollectionOfNullableElements; + + /** + * @var null|array + */ + public $nullableCollectionOfMultipleNonNullableElementTypes; + /** * @var array */ diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index 6cf083bb4ee86..754b6ae5fc4ec 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -118,15 +118,10 @@ private function createType(DocType $type, bool $nullable, string $docType = nul [$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen); - $key = $this->getTypes($type->getKeyType()); - $value = $this->getTypes($type->getValueType()); + $keys = $this->getTypes($type->getKeyType()); + $values = $this->getTypes($type->getValueType()); - // More than 1 type returned means it is a Compound type, which is - // not handled by Type, so better use a null value. - $key = 1 === \count($key) ? $key[0] : null; - $value = 1 === \count($value) ? $value[0] : null; - - return new Type($phpType, $nullable, $class, true, $key, $value); + return new Type($phpType, $nullable, $class, true, $keys, $values); } // Cannot guess @@ -134,27 +129,20 @@ private function createType(DocType $type, bool $nullable, string $docType = nul return null; } - if (str_ends_with($docType, '[]')) { - $collectionKeyType = new Type(Type::BUILTIN_TYPE_INT); - $collectionValueType = $this->createType($type, false, substr($docType, 0, -2)); + if (str_ends_with($docType, '[]') && $type instanceof Array_) { + $collectionKeyTypes = new Type(Type::BUILTIN_TYPE_INT); + $collectionValueTypes = $this->getTypes($type->getValueType()); - return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType); + return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes); } if ((str_starts_with($docType, 'list<') || str_starts_with($docType, 'array<')) && $type instanceof Array_) { // array is converted to x[] which is handled above // so it's only necessary to handle array here - $collectionKeyType = $this->getTypes($type->getKeyType())[0]; - + $collectionKeyTypes = $this->getTypes($type->getKeyType()); $collectionValueTypes = $this->getTypes($type->getValueType()); - if (1 != \count($collectionValueTypes)) { - // the Type class does not support union types yet, so assume that no type was defined - $collectionValueType = null; - } else { - $collectionValueType = $collectionValueTypes[0]; - } - return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType); + return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes); } if ($type instanceof PseudoType) { diff --git a/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php b/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php index 032821a18b69e..ec00bc1d9be6e 100644 --- a/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php @@ -13,6 +13,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface; +use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; use Symfony\Component\Security\Http\Event\LogoutEvent; /** @@ -31,6 +32,10 @@ public function __construct(ClearableTokenStorageInterface $csrfTokenStorage) public function onLogout(LogoutEvent $event): void { + if ($this->csrfTokenStorage instanceof SessionTokenStorage && !$event->getRequest()->hasPreviousSession()) { + return; + } + $this->csrfTokenStorage->clear(); } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfTokenClearingLogoutListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfTokenClearingLogoutListenerTest.php new file mode 100644 index 0000000000000..405c7ae085510 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfTokenClearingLogoutListenerTest.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\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; +use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Component\Security\Http\EventListener\CsrfTokenClearingLogoutListener; + +class CsrfTokenClearingLogoutListenerTest extends TestCase +{ + public function testSkipsClearingSessionTokenStorageOnStatelessRequest() + { + try { + (new CsrfTokenClearingLogoutListener( + new SessionTokenStorage(new RequestStack()) + ))->onLogout(new LogoutEvent(new Request(), null)); + } catch (SessionNotFoundException) { + $this->fail('clear() must not be called if the request is not associated with a session instance'); + } + + $this->addToAssertionCount(1); + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 6a0f290796b1d..e2b6c1bc8d511 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -322,6 +322,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes); if ($constructor) { + $context['has_constructor'] = true; if (true !== $constructor->isPublic()) { return $reflectionClass->newInstanceWithoutConstructor(); } @@ -412,6 +413,8 @@ protected function instantiateObject(array &$data, string $class, array &$contex } } + unset($context['has_constructor']); + return new $class(); } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index afd3acabc46a2..69dc534e417fb 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -128,8 +128,8 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory $this->propertyTypeExtractor = $propertyTypeExtractor; - if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) { - $classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + if ($classMetadataFactory) { + $classDiscriminatorResolver ??= new ClassDiscriminatorFromClassMetadata($classMetadataFactory); } $this->classDiscriminatorResolver = $classDiscriminatorResolver; $this->objectClassResolver = $objectClassResolver; @@ -217,7 +217,7 @@ public function normalize(mixed $object, string $format = null, array $context = } $preserveEmptyObjects = $context[self::PRESERVE_EMPTY_OBJECTS] ?? $this->defaultContext[self::PRESERVE_EMPTY_OBJECTS] ?? false; - if ($preserveEmptyObjects && !\count($data)) { + if ($preserveEmptyObjects && !$data) { return new \ArrayObject(); } @@ -226,19 +226,8 @@ public function normalize(mixed $object, string $format = null, array $context = protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, string $format = null) { - if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { - if (!isset($data[$mapping->getTypeProperty()])) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false); - } - - $type = $data[$mapping->getTypeProperty()]; - if (null === ($mappedClass = $mapping->getClassForType($type))) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true); - } - - if ($mappedClass !== $class) { - return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format); - } + if ($class !== $mappedClass = $this->getMappedClass($data, $class, $context)) { + return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format); } return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format); @@ -270,7 +259,7 @@ protected function getAttributes(object $object, ?string $format, array $context $attributes = $this->extractAttributes($object, $format, $context); - if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) { + if ($mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object)) { array_unshift($attributes, $mapping->getTypeProperty()); } @@ -319,11 +308,9 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $normalizedData = $this->prepareForDenormalization($data); $extraAttributes = []; - $reflectionClass = new \ReflectionClass($type); - $object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format); - $resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : $object::class; + $mappedClass = $this->getMappedClass($normalizedData, $type, $context); - $nestedAttributes = $this->getNestedAttributes($resolvedClass); + $nestedAttributes = $this->getNestedAttributes($mappedClass); $nestedData = []; $propertyAccessor = PropertyAccess::createPropertyAccessor(); foreach ($nestedAttributes as $property => $serializedPath) { @@ -336,6 +323,9 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $normalizedData = array_merge($normalizedData, $nestedData); + $object = $this->instantiateObject($normalizedData, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format); + $resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : $object::class; + foreach ($normalizedData as $attribute => $value) { if ($this->nameConverter) { $notConverted = $attribute; @@ -675,11 +665,8 @@ private function updateData(array $data, string $attribute, mixed $attributeValu */ private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool { - $enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false; - if ( - !$enableMaxDepth || - !isset($attributesMetadata[$attribute]) || - null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth() + if (!($enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false) + || null === $maxDepth = $attributesMetadata[$attribute]?->getMaxDepth() ) { return false; } @@ -755,7 +742,7 @@ private function isUninitializedValueError(\Error $e): bool */ private function getNestedAttributes(string $class): array { - if (!$this->classMetadataFactory || !$this->classMetadataFactory->hasMetadataFor($class)) { + if (!$this->classMetadataFactory?->hasMetadataFor($class)) { return []; } @@ -781,15 +768,30 @@ private function getNestedAttributes(string $class): array private function removeNestedValue(array $path, array $data): array { $element = array_shift($path); - if ([] === $path) { + if (!$path || !$data[$element] = $this->removeNestedValue($path, $data[$element])) { unset($data[$element]); - } else { - $data[$element] = $this->removeNestedValue($path, $data[$element]); - if ([] === $data[$element]) { - unset($data[$element]); - } } return $data; } + + /** + * @return class-string + */ + private function getMappedClass(array $data, string $class, array $context): string + { + if (!$mapping = $this->classDiscriminatorResolver?->getMappingForClass($class)) { + return $class; + } + + if (null === $type = $data[$mapping->getTypeProperty()] ?? null) { + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false); + } + + if (null === $mappedClass = $mapping->getClassForType($type)) { + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true); + } + + return $mappedClass; + } } diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php index 26943377b654a..18da1ae67d5ba 100644 --- a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php @@ -52,7 +52,11 @@ public function denormalize(mixed $data, string $type, string $format = null, ar try { return $type::from($data); } catch (\ValueError $e) { - throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type); + if (isset($context['has_constructor'])) { + throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type); + } + + throw NotNormalizableValueException::createForUnexpectedDataType('The data must belong to a backed enumeration of type '.$type, $data, [$type], $context['deserialization_path'] ?? null, true, 0, $e); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php index 096f766fadb77..b6e505b7ac518 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -83,7 +83,14 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $dateTimeFormat = $context[self::FORMAT_KEY] ?? null; $timezone = $this->getTimezone($context); - if (null === $data || !\is_string($data) || '' === trim($data)) { + if (\is_int($data) || \is_float($data)) { + switch ($dateTimeFormat) { + case 'U': $data = sprintf('%d', $data); break; + case 'U.u': $data = sprintf('%.6F', $data); break; + } + } + + if (!\is_string($data) || '' === trim($data)) { throw NotNormalizableValueException::createForUnexpectedDataType('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.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/SerializedPathInConstructorDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/SerializedPathInConstructorDummy.php new file mode 100644 index 0000000000000..a6d5109086899 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/SerializedPathInConstructorDummy.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures\Annotations; + +use Symfony\Component\Serializer\Annotation\SerializedPath; + +class SerializedPathInConstructorDummy +{ + public function __construct( + /** + * @SerializedPath("[one][two]") + */ + public $three, + ) { + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/SerializedPathInConstructorDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/SerializedPathInConstructorDummy.php new file mode 100644 index 0000000000000..90aee115417d4 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/SerializedPathInConstructorDummy.php @@ -0,0 +1,22 @@ + + * + * 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\Attributes; + +use Symfony\Component\Serializer\Annotation\SerializedPath; + +class SerializedPathInConstructorDummy +{ + public function __construct( + #[SerializedPath('[one][two]')] public $three, + ) { + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumProperty.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumProperty.php new file mode 100644 index 0000000000000..f2677195f2820 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumProperty.php @@ -0,0 +1,10 @@ + + + + + diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml index 7519b979efa96..e052d65a88779 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml @@ -22,6 +22,10 @@ serialized_path: '[one][two]' seven: serialized_path: '[three][four]' +'Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy': + attributes: + three: + serialized_path: '[one][two]' 'Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummy': discriminator_map: type_property: type diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php index 5ce1931ba0cab..903612c684c0d 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\Annotations\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedNameDummy; use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathDummy; +use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy; use Symfony\Component\Serializer\Tests\Fixtures\Dummy; final class ClassMetadataFactoryCompilerTest extends TestCase @@ -46,18 +47,20 @@ public function testItDumpMetadata() $maxDepthDummyMetadata = $classMetatadataFactory->getMetadataFor(MaxDepthDummy::class); $serializedNameDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedNameDummy::class); $serializedPathDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathDummy::class); + $serializedPathInConstructorDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathInConstructorDummy::class); $code = (new ClassMetadataFactoryCompiler())->compile([ $dummyMetadata, $maxDepthDummyMetadata, $serializedNameDummyMetadata, $serializedPathDummyMetadata, + $serializedPathInConstructorDummyMetadata, ]); file_put_contents($this->dumpPath, $code); $compiledMetadata = require $this->dumpPath; - $this->assertCount(4, $compiledMetadata); + $this->assertCount(5, $compiledMetadata); $this->assertArrayHasKey(Dummy::class, $compiledMetadata); $this->assertEquals([ @@ -99,5 +102,13 @@ public function testItDumpMetadata() ], null, ], $compiledMetadata[SerializedPathDummy::class]); + + $this->assertArrayHasKey(SerializedPathInConstructorDummy::class, $compiledMetadata); + $this->assertEquals([ + [ + 'three' => [[], null, null, '[one][two]'], + ], + null, + ], $compiledMetadata[SerializedPathInConstructorDummy::class]); } } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTestCase.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTestCase.php index de7accd844b32..2dbd03703a2ce 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTestCase.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTestCase.php @@ -103,6 +103,15 @@ public function testLoadSerializedPath() $this->assertEquals(new PropertyPath('[three][four]'), $attributesMetadata['seven']->getSerializedPath()); } + public function testLoadSerializedPathInConstructor() + { + $classMetadata = new ClassMetadata($this->getNamespace().'\SerializedPathInConstructorDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals(new PropertyPath('[one][two]'), $attributesMetadata['three']->getSerializedPath()); + } + public function testLoadClassMetadataAndMerge() { $classMetadata = new ClassMetadata($this->getNamespace().'\GroupDummy'); diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php index b1e9ed7222636..202534f56fca5 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -94,6 +94,15 @@ public function testSerializedPath() $this->assertEquals('[three][four]', $attributesMetadata['seven']->getSerializedPath()); } + public function testSerializedPathInConstructor() + { + $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals('[one][two]', $attributesMetadata['three']->getSerializedPath()); + } + public function testLoadDiscriminatorMap() { $classMetadata = new ClassMetadata(AbstractDummy::class); diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php index bbe0a99aeab89..dcfd2b4afac51 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -108,6 +108,15 @@ public function testSerializedPath() $this->assertEquals(new PropertyPath('[three][four]'), $attributesMetadata['seven']->getSerializedPath()); } + public function testSerializedPathInConstructor() + { + $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals(new PropertyPath('[one][two]'), $attributesMetadata['three']->getSerializedPath()); + } + public function testLoadDiscriminatorMap() { $classMetadata = new ClassMetadata(AbstractDummy::class); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 0c342b64d8000..d876193c8a741 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Annotation\Context; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Annotation\SerializedName; use Symfony\Component\Serializer\Annotation\SerializedPath; use Symfony\Component\Serializer\Exception\ExtraAttributesException; @@ -171,6 +172,53 @@ public function testDenormalizeWithNestedAttributesDuplicateKeys() $normalizer->denormalize($data, DuplicateKeyNestedDummy::class, 'any'); } + public function testDenormalizeWithNestedAttributesInConstructor() + { + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $data = [ + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ]; + $test = $normalizer->denormalize($data, NestedDummyWithConstructor::class, 'any'); + $this->assertSame('foo', $test->foo); + $this->assertSame('quux', $test->quux); + $this->assertSame('notfoo', $test->notfoo); + $this->assertSame('baz', $test->baz); + } + + public function testDenormalizeWithNestedAttributesInConstructorAndDiscriminatorMap() + { + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $data = [ + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ]; + + $test1 = $normalizer->denormalize($data + ['type' => 'first'], AbstractNestedDummyWithConstructorAndDiscriminator::class, 'any'); + $this->assertInstanceOf(FirstNestedDummyWithConstructorAndDiscriminator::class, $test1); + $this->assertSame('foo', $test1->foo); + $this->assertSame('notfoo', $test1->notfoo); + $this->assertSame('baz', $test1->baz); + + $test2 = $normalizer->denormalize($data + ['type' => 'second'], AbstractNestedDummyWithConstructorAndDiscriminator::class, 'any'); + $this->assertInstanceOf(SecondNestedDummyWithConstructorAndDiscriminator::class, $test2); + $this->assertSame('quux', $test2->quux); + $this->assertSame('notfoo', $test2->notfoo); + $this->assertSame('baz', $test2->baz); + } + public function testNormalizeWithNestedAttributesMixingArrayTypes() { $this->expectException(LogicException::class); @@ -236,6 +284,52 @@ public function testNormalizeWithNestedAttributesWithoutMetadata() $this->assertSame($data, $test); } + public function testNormalizeWithNestedAttributesInConstructor() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $test = $normalizer->normalize(new NestedDummyWithConstructor('foo', 'quux', 'notfoo', 'baz'), 'any'); + $this->assertSame([ + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ], $test); + } + + public function testNormalizeWithNestedAttributesInConstructorAndDiscriminatorMap() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $test1 = $normalizer->normalize(new FirstNestedDummyWithConstructorAndDiscriminator('foo', 'notfoo', 'baz'), 'any'); + $this->assertSame([ + 'type' => 'first', + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ], $test1); + + $test2 = $normalizer->normalize(new SecondNestedDummyWithConstructorAndDiscriminator('quux', 'notfoo', 'baz'), 'any'); + $this->assertSame([ + 'type' => 'second', + 'one' => [ + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ], $test2); + } + public function testDenormalizeCollectionDecodedFromXmlWithOneChild() { $denormalizer = $this->getDenormalizerForDummyCollection(); @@ -661,6 +755,78 @@ class NestedDummy public $baz; } +class NestedDummyWithConstructor +{ + public function __construct( + /** + * @SerializedPath("[one][two][three]") + */ + public $foo, + + /** + * @SerializedPath("[one][four]") + */ + public $quux, + + /** + * @SerializedPath("[foo]") + */ + public $notfoo, + + public $baz, + ) { + } +} + +/** + * @DiscriminatorMap(typeProperty="type", mapping={ + * "first" = FirstNestedDummyWithConstructorAndDiscriminator::class, + * "second" = SecondNestedDummyWithConstructorAndDiscriminator::class, + * }) + */ +abstract class AbstractNestedDummyWithConstructorAndDiscriminator +{ + public function __construct( + /** + * @SerializedPath("[foo]") + */ + public $notfoo, + + public $baz, + ) { + } +} + +class FirstNestedDummyWithConstructorAndDiscriminator extends AbstractNestedDummyWithConstructorAndDiscriminator +{ + public function __construct( + /** + * @SerializedPath("[one][two][three]") + */ + public $foo, + + $notfoo, + $baz, + ) { + parent::__construct($notfoo, $baz); + } +} + +class SecondNestedDummyWithConstructorAndDiscriminator extends AbstractNestedDummyWithConstructorAndDiscriminator +{ + public function __construct( + /** + * @SerializedPath("[one][four]") + */ + public $quux, + + $notfoo, + $baz, + ) { + parent::__construct($notfoo, $baz); + } +} + class DuplicateKeyNestedDummy { /** @@ -711,7 +877,9 @@ protected function getAttributeValue(object $object, string $attribute, string $ protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []) { - $object->$attribute = $value; + if (property_exists($object, $attribute)) { + $object->$attribute = $value; + } } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php index b0063da5fe4e7..ef58a7f25b8fe 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php @@ -88,7 +88,7 @@ public function testDenormalizeObjectThrowsException() public function testDenormalizeBadBackingValueThrowsException() { - $this->expectException(InvalidArgumentException::class); + $this->expectException(NotNormalizableValueException::class); $this->expectExceptionMessage('The data must belong to a backed enumeration of type '.StringBackedEnumDummy::class); $this->normalizer->denormalize('POST', StringBackedEnumDummy::class); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php index 674dfaab5382d..25b7c784fe0e2 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php @@ -178,6 +178,8 @@ public function testDenormalize() $this->assertEquals(new \DateTimeImmutable('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTimeImmutable::class)); $this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTime::class)); $this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize(' 2016-01-01T00:00:00+00:00 ', \DateTime::class)); + $this->assertEquals(new \DateTimeImmutable('2023-05-06T17:35:34.000000+0000', new \DateTimeZone('UTC')), $this->normalizer->denormalize(1683394534, \DateTimeImmutable::class, null, [DateTimeNormalizer::FORMAT_KEY => 'U'])); + $this->assertEquals(new \DateTimeImmutable('2023-05-06T17:35:34.123400+0000', new \DateTimeZone('UTC')), $this->normalizer->denormalize(1683394534.1234, \DateTimeImmutable::class, null, [DateTimeNormalizer::FORMAT_KEY => 'U.u'])); } public function testDenormalizeUsingTimezonePassedInConstructor() diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 908f4b352a567..ee2693e54e42a 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -60,6 +60,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo; use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor; +use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty; use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy; use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74Full; @@ -1232,7 +1233,48 @@ public function testCollectDenormalizationErrorsWithEnumConstructor() $this->assertSame($expected, $exceptionsAsArray); } - public function testNoCollectDenormalizationErrorsWithWrongEnum() + public function testCollectDenormalizationErrorsWithWrongPropertyWithoutConstruct() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader()); + $reflectionExtractor = new ReflectionExtractor(); + $propertyInfoExtractor = new PropertyInfoExtractor([], [$reflectionExtractor], [], [], []); + + $serializer = new Serializer( + [ + new BackedEnumNormalizer(), + new ObjectNormalizer($classMetadataFactory, null, null, $propertyInfoExtractor), + ], + ['json' => new JsonEncoder()] + ); + + try { + $serializer->deserialize('{"get": "POST"}', DummyObjectWithEnumProperty::class, 'json', [ + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); + } catch (\Throwable $e) { + $this->assertInstanceOf(PartialDenormalizationException::class, $e); + } + + $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array { + return [ + 'currentType' => $e->getCurrentType(), + 'useMessageForUser' => $e->canUseMessageForUser(), + 'message' => $e->getMessage(), + ]; + }, $e->getErrors()); + + $expected = [ + [ + 'currentType' => 'string', + 'useMessageForUser' => true, + 'message' => 'The data must belong to a backed enumeration of type Symfony\Component\Serializer\Tests\Fixtures\StringBackedEnumDummy', + ], + ]; + + $this->assertSame($expected, $exceptionsAsArray); + } + + public function testNoCollectDenormalizationErrorsWithWrongEnumOnConstructor() { $serializer = new Serializer( [ @@ -1243,7 +1285,7 @@ public function testNoCollectDenormalizationErrorsWithWrongEnum() ); try { - $serializer->deserialize('{"get": "invalid"}', DummyObjectWithEnumConstructor::class, 'json', [ + $serializer->deserialize('{"get": "POST"}', DummyObjectWithEnumConstructor::class, 'json', [ DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, ]); } catch (\Throwable $th) { diff --git a/src/Symfony/Component/Translation/Loader/ArrayLoader.php b/src/Symfony/Component/Translation/Loader/ArrayLoader.php index e240493953731..e63a7d05bec94 100644 --- a/src/Symfony/Component/Translation/Loader/ArrayLoader.php +++ b/src/Symfony/Component/Translation/Loader/ArrayLoader.php @@ -43,9 +43,11 @@ private function flatten(array $messages): array foreach ($messages as $key => $value) { if (\is_array($value)) { foreach ($this->flatten($value) as $k => $v) { - $result[$key.'.'.$k] = $v; + if (null !== $v) { + $result[$key.'.'.$k] = $v; + } } - } else { + } elseif (null !== $value) { $result[$key] = $value; } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php index 230c02e539e45..cac0b6f87823a 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php @@ -30,6 +30,15 @@ public function testLoad() $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } + public function testLoadNonStringMessages() + { + $loader = new YamlFileLoader(); + $resource = __DIR__.'/../fixtures/non-string.yml'; + $catalogue = $loader->load($resource, 'en', 'domain1'); + + $this->assertSame(['root.foo2' => '', 'root.bar' => 'bar'], $catalogue->all('domain1')); + } + public function testLoadDoesNothingIfEmpty() { $loader = new YamlFileLoader(); diff --git a/src/Symfony/Component/Translation/Tests/fixtures/non-string.yml b/src/Symfony/Component/Translation/Tests/fixtures/non-string.yml new file mode 100644 index 0000000000000..41e245e19a4a6 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/non-string.yml @@ -0,0 +1,4 @@ +root: + foo1: + foo2: '' + bar: 'bar' diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index d6e690c02e8b5..f1a00aa72ca39 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -44,7 +44,6 @@ "conflict": { "doctrine/annotations": "<1.13", "doctrine/lexer": "<1.1", - "phpunit/phpunit": "<5.4.3", "symfony/dependency-injection": "<5.4", "symfony/expression-language": "<5.4", "symfony/http-kernel": "<5.4", diff --git a/src/Symfony/Component/VarDumper/composer.json b/src/Symfony/Component/VarDumper/composer.json index 71ec64c0de917..787f7c0b34b49 100644 --- a/src/Symfony/Component/VarDumper/composer.json +++ b/src/Symfony/Component/VarDumper/composer.json @@ -27,7 +27,6 @@ "twig/twig": "^2.13|^3.0.4" }, "conflict": { - "phpunit/phpunit": "<5.4.3", "symfony/console": "<5.4" }, "suggest": {
  • LocaleLocaleFallback localeDomainDomain Times used Message ID Message Preview
    {{ message.locale }}{{ message.fallbackLocale|default('-') }}