diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 90e51d60536d6..3d21822287b6b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 7.3 for features / 5.4, 6.4, 7.1, and 7.2 for bug fixes +| Branch? | 7.3 for features / 6.4, 7.1, and 7.2 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index e5defe2a989f9..8849fd3a94c58 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -33,6 +33,7 @@ jobs: mode: low-deps - php: '8.3' - php: '8.4' + - php: '8.5' #mode: experimental fail-fast: false diff --git a/CHANGELOG-6.4.md b/CHANGELOG-6.4.md index 94111d16ed62b..56df1b333f50b 100644 --- a/CHANGELOG-6.4.md +++ b/CHANGELOG-6.4.md @@ -7,6 +7,25 @@ in 6.4 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v6.4.0...v6.4.1 +* 6.4.17 (2024-12-31) + + * bug #59304 [PropertyInfo] Remove ``@internal`` from `PropertyReadInfo` and `PropertyWriteInfo` (Dario Guarracino) + * bug #59318 [Finder] Fix using `==` as default operator in `DateComparator` (MatTheCat) + * bug #59321 [HtmlSanitizer] reject URLs containing whitespaces (xabbuh) + * bug #59250 [HttpClient] Fix a typo in NoPrivateNetworkHttpClient (Jontsa) + * bug #59103 [Messenger] ensure exception on rollback does not hide previous exception (nikophil) + * bug #59226 [FrameworkBundle] require the writer to implement getFormats() in the translation:extract (xabbuh) + * bug #59213 [FrameworkBundle] don't require fake notifier transports to be installed as non-dev dependencies (xabbuh) + * bug #59160 [BeanstalkMessenger] Round delay to an integer to avoid deprecation warning (plantas) + * bug #59012 [PropertyInfo] Fix interface handling in `PhpStanTypeHelper` (janedbal) + * bug #59134 [HttpKernel] Denormalize request data using the csv format when using "#[MapQueryString]" or "#[MapRequestPayload]" (except for content data) (ovidiuenache) + * bug #59140 [WebProfilerBundle] fix: white-space in highlighted code (chr-hertel) + * bug #59124 [FrameworkBundle] fix: notifier push channel bus abstract arg (raphael-geffroy) + * bug #59069 [Console] Fix division by 0 error (Rindula) + * bug #59070 [PropertyInfo] evaluate access flags for properties with asymmetric visibility (xabbuh) + * bug #59062 [HttpClient] Always set CURLOPT_CUSTOMREQUEST to the correct HTTP method in CurlHttpClient (KurtThiemann) + * bug #59023 [HttpClient] Fix streaming and redirecting with NoPrivateNetworkHttpClient (nicolas-grekas) + * 6.4.16 (2024-11-27) * bug #59013 [HttpClient] Fix checking for private IPs before connecting (nicolas-grekas) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bcc33dc4892f2..d0472fa4bd167 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -19,8 +19,8 @@ The Symfony Connect username in parenthesis allows to get more information - Jordi Boggiano (seldaek) - Maxime Steinhausser (ogizanagi) - Kévin Dunglas (dunglas) - - Victor Berchet (victor) - Javier Eguiluz (javier.eguiluz) + - Victor Berchet (victor) - Ryan Weaver (weaverryan) - Jérémy DERUSSÉ (jderusse) - Jules Pietri (heah) @@ -34,8 +34,8 @@ The Symfony Connect username in parenthesis allows to get more information - Tobias Nyholm (tobias) - HypeMC (hypemc) - Jérôme Tamarelle (gromnan) - - Samuel ROZE (sroze) - Antoine Lamirault (alamirault) + - Samuel ROZE (sroze) - Pascal Borreli (pborreli) - Romain Neutron - Joseph Bielawski (stloyd) @@ -51,15 +51,15 @@ The Symfony Connect username in parenthesis allows to get more information - Igor Wiedler - Jan Schädlich (jschaedl) - Mathieu Lechat (mat_the_cat) + - Mathias Arlaud (mtarld) + - Simon André (simonandre) - Matthias Pigulla (mpdude) - Gabriel Ostrolucký (gadelat) - - Simon André (simonandre) - Jonathan Wage (jwage) - Vincent Langlet (deviling) - Valentin Udaltsov (vudaltsov) - - Mathias Arlaud (mtarld) - - Alexandre Salomé (alexandresalome) - Grégoire Paris (greg0ire) + - Alexandre Salomé (alexandresalome) - William DURAND - ornicar - Dany Maillard (maidmaid) @@ -73,21 +73,21 @@ The Symfony Connect username in parenthesis allows to get more information - Pierre du Plessis (pierredup) - David Maicher (dmaicher) - Tomasz Kowalczyk (thunderer) + - Mathieu Santostefano (welcomattic) - Bulat Shakirzyanov (avalanche123) - Iltar van der Berg - Miha Vrhovnik (mvrhov) - Gary PEGEOT (gary-p) - - Mathieu Santostefano (welcomattic) - Saša Stamenković (umpirsky) - Allison Guilhem (a_guilhem) - Alexander Schranz (alexander-schranz) + - Dariusz Ruminski - Mathieu Piot (mpiot) - Vasilij Duško (staff) - Sarah Khalil (saro0h) - Laurent VOULLEMIER (lvo) - Konstantin Kudryashov (everzet) - Guilhem N (guilhemn) - - Dariusz Ruminski - Bilal Amarni (bamarni) - Eriksen Costa - Florin Patan (florinpatan) @@ -110,12 +110,12 @@ The Symfony Connect username in parenthesis allows to get more information - Baldini - Alex Pott - Fran Moreno (franmomu) + - Hubert Lenoir (hubert_lenoir) - Charles Sarrazin (csarrazi) - Henrik Westphal (snc) - Dariusz Górecki (canni) - - Hubert Lenoir (hubert_lenoir) - - Ener-Getick - Antoine Makdessi (amakdessi) + - Ener-Getick - Graham Campbell (graham) - Tugdual Saunier (tucksaun) - Lee McDermott @@ -144,20 +144,20 @@ The Symfony Connect username in parenthesis allows to get more information - Tac Tacelosky (tacman1123) - gnito-org - Tim Nagel (merk) + - Valtteri R (valtzu) - Chris Wilkinson (thewilkybarkid) - Jérôme Vasseur (jvasseur) - Peter Kokot (peterkokot) - Brice BERNARD (brikou) + - Jacob Dreesen (jdreesen) + - Nicolas Philippe (nikophil) - Martin Auswöger - Michal Piotrowski - marc.weistroff - Lars Strojny (lstrojny) - lenar - Vladimir Tsykun (vtsykun) - - Jacob Dreesen (jdreesen) - Włodzimierz Gajda (gajdaw) - - Valtteri R (valtzu) - - Nicolas Philippe (nikophil) - Javier Spagnoletti (phansys) - Adrien Brault (adrienbrault) - Florian Voutzinos (florianv) @@ -170,6 +170,7 @@ The Symfony Connect username in parenthesis allows to get more information - Baptiste Clavié (talus) - Alexander Schwenn (xelaris) - Fabien Pennequin (fabienpennequin) + - Dāvis Zālītis (k0d3r1s) - Gordon Franke (gimler) - Malte Schlüter (maltemaltesich) - jeremyFreeAgent (jeremyfreeagent) @@ -178,14 +179,15 @@ The Symfony Connect username in parenthesis allows to get more information - Vasilij Dusko - Daniel Wehner (dawehner) - Maxime Helias (maxhelias) - - Dāvis Zālītis (k0d3r1s) - Robert Schönthal (digitalkaoz) - Smaine Milianni (ismail1432) + - Hugo Alliaume (kocal) - François-Xavier de Guillebon (de-gui_f) - Andreas Schempp (aschempp) - noniagriconomie - Eric GELOEN (gelo) - Gabriel Caruso + - Christopher Hertel (chertel) - Stefano Sala (stefano.sala) - Ion Bazan (ionbazan) - Niels Keurentjes (curry684) @@ -193,20 +195,19 @@ The Symfony Connect username in parenthesis allows to get more information - Jhonny Lidfors (jhonne) - Juti Noppornpitak (shiroyuki) - Gregor Harlan (gharlan) - - Hugo Alliaume (kocal) + - Alexis Lefebvre - Anthony MARTIN - Sebastian Hörl (blogsh) - Tigran Azatyan (tigranazatyan) - Florent Mata (fmata) - - Christopher Hertel (chertel) - Jonathan Scheiber (jmsche) - Daniel Gomes (danielcsgomes) - Hidenori Goto (hidenorigoto) + - Thomas Landauer (thomas-landauer) - Arnaud Kleinpeter (nanocom) - Guilherme Blanco (guilhermeblanco) - Saif Eddin Gmati (azjezz) - Farhad Safarov (safarov) - - Alexis Lefebvre - SpacePossum - Richard van Laak (rvanlaak) - Andreas Braun @@ -214,7 +215,6 @@ The Symfony Connect username in parenthesis allows to get more information - Alessandro Chitolina (alekitto) - Rafael Dohms (rdohms) - Roman Martinuk (a2a4) - - Thomas Landauer (thomas-landauer) - jwdeitch - David Prévot (taffit) - Jérôme Parmentier (lctrs) @@ -223,6 +223,7 @@ The Symfony Connect username in parenthesis allows to get more information - soyuka - Jérémy Derussé - Matthieu Napoli (mnapoli) + - Bob van de Vijver (bobvandevijver) - Tomas Votruba (tomas_votruba) - Arman Hosseini (arman) - Sokolov Evgeniy (ewgraf) @@ -242,7 +243,6 @@ The Symfony Connect username in parenthesis allows to get more information - Fabien Bourigault (fbourigault) - Olivier Dolbeau (odolbeau) - Rouven Weßling (realityking) - - Bob van de Vijver (bobvandevijver) - Daniel Burger - Ben Davies (bendavies) - YaFou @@ -270,6 +270,7 @@ The Symfony Connect username in parenthesis allows to get more information - Samuel NELA (snela) - Baptiste Leduc (korbeil) - Vincent AUBERT (vincent) + - Nate Wiebe (natewiebe13) - Michael Voříšek - zairig imad (zairigimad) - Colin O'Dell (colinodell) @@ -336,7 +337,6 @@ The Symfony Connect username in parenthesis allows to get more information - Julien Pauli - Michael Lee (zerustech) - Florian Lonqueu-Brochard (florianlb) - - Nate Wiebe (natewiebe13) - Joe Bennett (kralos) - Leszek Prabucki (l3l0) - Wojciech Kania @@ -351,6 +351,7 @@ The Symfony Connect username in parenthesis allows to get more information - fd6130 (fdtvui) - Priyadi Iman Nurcahyo (priyadi) - Alan Poulain (alanpoulain) + - Oleg Andreyev (oleg.andreyev) - Maciej Malarz (malarzm) - Marcin Sikoń (marphi) - Michele Orselli (orso) @@ -390,13 +391,14 @@ The Symfony Connect username in parenthesis allows to get more information - Alexander Kotynia (olden) - Elnur Abdurrakhimov (elnur) - Manuel Reinhard (sprain) + - Zan Baldwin (zanbaldwin) + - Tim Goudriaan (codedmonkey) - Antonio J. García Lagar (ajgarlag) - BoShurik - Quentin Devos - Adam Prager (padam87) - Benoît Burnichon (bburnichon) - maxime.steinhausser - - Oleg Andreyev (oleg.andreyev) - Roman Ring (inori) - Xavier Montaña Carreras (xmontana) - Arjen van der Meijden @@ -415,6 +417,7 @@ The Symfony Connect username in parenthesis allows to get more information - Pierre-Yves Lebecq (pylebecq) - Benjamin Leveque (benji07) - Jordan Samouh (jordansamouh) + - David Badura (davidbadura) - Sullivan SENECHAL (soullivaneuh) - Uwe Jäger (uwej711) - javaDeveloperKid @@ -458,9 +461,7 @@ The Symfony Connect username in parenthesis allows to get more information - Wodor Wodorski - Beau Simensen (simensen) - Magnus Nordlander (magnusnordlander) - - Tim Goudriaan (codedmonkey) - Robert Kiss (kepten) - - Zan Baldwin (zanbaldwin) - Alexandre Quercia (alquerci) - Marcos Sánchez - Emanuele Panzeri (thepanz) @@ -483,10 +484,11 @@ The Symfony Connect username in parenthesis allows to get more information - Marco Petersen (ocrampete16) - Bohan Yang (brentybh) - Vilius Grigaliūnas - - David Badura (davidbadura) + - Jordane VASPARD (elementaire) - Chris Smith (cs278) - Thomas Bisignani (toma) - Florian Klein (docteurklein) + - Raphaël Geffroy (raphael-geffroy) - Damien Alexandre (damienalexandre) - Manuel Kießling (manuelkiessling) - Alexey Kopytko (sanmai) @@ -582,7 +584,6 @@ The Symfony Connect username in parenthesis allows to get more information - Alexander Menshchikov - Clément Gautier (clementgautier) - roman joly (eltharin) - - Jordane VASPARD (elementaire) - James Gilliland (neclimdul) - Sanpi (sanpi) - Eduardo Gulias (egulias) @@ -683,9 +684,11 @@ The Symfony Connect username in parenthesis allows to get more information - Neil Peyssard (nepey) - Niklas Fiekas - Mark Challoner (markchalloner) + - Andreas Hennings - Markus Bachmann (baachi) - Gunnstein Lye (glye) - Erkhembayar Gantulga (erheme318) + - Yi-Jyun Pan - Sergey Melesh (sergex) - Greg Anderson - lancergr @@ -761,6 +764,7 @@ The Symfony Connect username in parenthesis allows to get more information - Soufian EZ ZANTAR (soezz) - Marek Zajac - Adam Harvey + - Klaus Silveira (klaussilveira) - ilyes kooli (skafandri) - Anton Bakai - battye @@ -787,6 +791,7 @@ The Symfony Connect username in parenthesis allows to get more information - Joshua Nye - Martin Kirilov (wucdbm) - Koen Reiniers (koenre) + - Kurt Thiemann - Nathan Dench (ndenc2) - Gijs van Lammeren - Sebastian Bergmann @@ -797,6 +802,7 @@ The Symfony Connect username in parenthesis allows to get more information - Kev - Kevin McBride - Sergio Santoro + - Jonas Elfering - Philipp Rieber (bicpi) - Dmitriy Derepko - Manuel de Ruiter (manuel) @@ -899,6 +905,7 @@ The Symfony Connect username in parenthesis allows to get more information - Daniel Tiringer - Lenar Lõhmus - Ilija Tovilo (ilijatovilo) + - Maxime Pinot (maximepinot) - Sander Toonen (xatoo) - Zach Badgett (zachbadgett) - Loïc Faugeron @@ -949,7 +956,6 @@ The Symfony Connect username in parenthesis allows to get more information - Franck RANAIVO-HARISOA (franckranaivo) - Yi-Jyun Pan - Egor Taranov - - Andreas Hennings - Arnaud Frézet - Philippe Segatori - Jon Gotlin (jongotlin) @@ -966,6 +972,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ricky Su (ricky) - scyzoryck - Kyle Evans (kevans91) + - Ioan Ovidiu Enache (ionutenache) - Max Rath (drak3) - Cristoforo Cervino (cristoforocervino) - marie @@ -975,7 +982,6 @@ The Symfony Connect username in parenthesis allows to get more information - Noémi Salaün (noemi-salaun) - Sinan Eldem (sineld) - Gennady Telegin - - Yi-Jyun Pan - ampaze - Alexandre Dupuy (satchette) - Michel Hunziker @@ -998,6 +1004,7 @@ The Symfony Connect username in parenthesis allows to get more information - Åsmund Garfors - Maxime Douailin - Jean Pasdeloup + - Maxime COLIN (maximecolin) - Lorenzo Millucci (lmillucci) - Javier López (loalf) - Reinier Kip @@ -1064,6 +1071,7 @@ The Symfony Connect username in parenthesis allows to get more information - Quentin de Longraye (quentinus95) - Chris Heng (gigablah) - Mickaël Buliard (mbuliard) + - Jan Nedbal - Cornel Cruceru (amne) - Richard Bradley - Jan Walther (janwalther) @@ -1156,7 +1164,6 @@ The Symfony Connect username in parenthesis allows to get more information - Aleksandr Volochnev (exelenz) - Robin van der Vleuten (robinvdvleuten) - Grinbergs Reinis (shima5) - - Klaus Silveira (klaussilveira) - Michael Piecko (michael.piecko) - Toni Peric (tperic) - yclian @@ -1241,7 +1248,6 @@ The Symfony Connect username in parenthesis allows to get more information - Thorry84 - Romanavr - michaelwilliams - - Raphaël Geffroy (raphael-geffroy) - Alexandre Parent - 1emming - Nykopol (nykopol) @@ -1295,6 +1301,7 @@ The Symfony Connect username in parenthesis allows to get more information - _sir_kane (waly) - Olivier Maisonneuve - Gálik Pál + - Bálint Szekeres - Andrei C. (moldman) - Mike Meier (mykon) - Pedro Miguel Maymone de Resende (pedroresende) @@ -1306,6 +1313,7 @@ The Symfony Connect username in parenthesis allows to get more information - Kagan Balga (kagan-balga) - Nikita Nefedov (nikita2206) - Alex Bacart + - StefanoTarditi - cgonzalez - hugovms - Ben @@ -1342,6 +1350,7 @@ The Symfony Connect username in parenthesis allows to get more information - Francisco Alvarez (sormes) - Martin Parsiegla (spea) - Maxim Tugaev (tugmaks) + - ywisax - Manuel Alejandro Paz Cetina - Denis Charrier (brucewouaigne) - Youssef Benhssaien (moghreb) @@ -1418,6 +1427,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jason Woods - mwsaz - bogdan + - wanxiangchwng - Geert De Deckere - grizlik - Derek ROTH @@ -1432,7 +1442,6 @@ The Symfony Connect username in parenthesis allows to get more information - Dmytro Boiko (eagle) - Shin Ohno (ganchiku) - Matthieu Mota (matthieumota) - - Maxime Pinot (maximepinot) - Jean-Baptiste GOMOND (mjbgo) - Jakub Podhorsky (podhy) - abdul malik ikhsan (samsonasik) @@ -1447,7 +1456,6 @@ The Symfony Connect username in parenthesis allows to get more information - Morten Wulff (wulff) - Kieran - Don Pinkster - - Jonas Elfering - Maksim Muruev - Emil Einarsson - 243083df @@ -1600,7 +1608,6 @@ The Symfony Connect username in parenthesis allows to get more information - Grégoire Hébert (gregoirehebert) - Franz Wilding (killerpoke) - Ferenczi Krisztian (fchris82) - - Ioan Ovidiu Enache (ionutenache) - Artyum Petrov - Oleg Golovakhin (doc_tr) - Guillaume Smolders (guillaumesmo) @@ -1624,6 +1631,7 @@ The Symfony Connect username in parenthesis allows to get more information - Luciano Mammino (loige) - LHommet Nicolas (nicolaslh) - fabios + - eRIZ - Sander Coolen (scoolen) - Vic D'Elfant (vicdelfant) - Amirreza Shafaat (amirrezashafaat) @@ -1779,6 +1787,7 @@ The Symfony Connect username in parenthesis allows to get more information - Evgeny Anisiforov - otsch - TristanPouliquen + - Dominic Luidold - Piotr Antosik (antek88) - Nacho Martin (nacmartin) - Thibaut Chieux @@ -1824,6 +1833,7 @@ The Symfony Connect username in parenthesis allows to get more information - Claus Due (namelesscoder) - Christian - Alexandru Patranescu + - Sébastien Lévêque (legenyes) - ju1ius - Denis Golubovskiy (bukashk0zzz) - Arkadiusz Rzadkowolski (flies) @@ -2034,6 +2044,7 @@ The Symfony Connect username in parenthesis allows to get more information - Vladimir Mantulo (mantulo) - Boullé William (williamboulle) - Jesper Noordsij + - Bart Baaten - Frederic Godfrin - Paul Matthews - aim8604 @@ -2068,6 +2079,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dalibor Karlović - Cesar Scur (cesarscur) - Cyril Vermandé (cyve) + - Daniele Orru' (danydev) - Raul Garcia Canet (juagarc4) - Sagrario Meneses - Dmitri Petmanson @@ -2161,6 +2173,7 @@ The Symfony Connect username in parenthesis allows to get more information - Maxime THIRY - Norman Soetbeer - Ludek Stepan + - Benjamin BOUDIER - Frederik Schwan - Mark van den Berg - Aaron Stephens (astephens) @@ -2191,6 +2204,7 @@ The Symfony Connect username in parenthesis allows to get more information - Harald Tollefsen - PabloKowalczyk - Matthieu + - ZiYao54 - Arend-Jan Tetteroo - Albin Kerouaton - Sébastien HOUZÉ @@ -2250,6 +2264,7 @@ The Symfony Connect username in parenthesis allows to get more information - George Giannoulopoulos - Alexander Pasichnik (alex_brizzz) - Florian Merle (florian-merle) + - Felix Eymonot (hyanda) - Luis Ramirez (luisdeimos) - Ilia Sergunin (maranqz) - Daniel Richter (richtermeister) @@ -2276,6 +2291,7 @@ The Symfony Connect username in parenthesis allows to get more information - Frank Neff (fneff) - Volodymyr Kupriienko (greeflas) - Ilya Biryukov (ibiryukov) + - Mathieu Ledru (matyo91) - Roma (memphys) - Florian Caron (shalalalala) - Serhiy Lunak (slunak) @@ -2374,6 +2390,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ivan Tse - René Kerner - Nathaniel Catchpole + - Igor Plantaš - upchuk - Adrien Samson (adriensamson) - Samuel Gordalina (gordalina) @@ -2381,7 +2398,6 @@ The Symfony Connect username in parenthesis allows to get more information - Nicolas Eeckeloo (neeckeloo) - Andriy Prokopenko (sleepyboy) - Dariusz Ruminski - - Bálint Szekeres - Starfox64 - Ivo Valchev - Thomas Hanke @@ -2394,9 +2410,11 @@ The Symfony Connect username in parenthesis allows to get more information - Wojciech Gorczyca - Neagu Cristian-Doru (cristian-neagu) - Mathieu Morlon (glutamatt) + - NIRAV MUKUNDBHAI PATEL (niravpatel919) - Owen Gray (otis) - Rafał Muszyński (rafmus90) - Sébastien Decrême (sebdec) + - Wu (wu-agriconomie) - Timothy Anido (xanido) - Robert-Jan de Dreu - Mara Blaga @@ -2481,6 +2499,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jeremiah VALERIE - Alexandre Beaujour - Franck Ranaivo-Harisoa + - Grégoire Rabasse - Cas van Dongen - Patrik Patie Gmitter - George Yiannoulopoulos @@ -2560,6 +2579,7 @@ The Symfony Connect username in parenthesis allows to get more information - Tobias Genberg (lorceroth) - Michael Simonson (mikes) - Nicolas Badey (nico-b) + - Florent Blaison (orkin) - Olivier Scherler (oscherler) - Flo Gleixner (redflo) - Romain Jacquart (romainjacquart) @@ -2599,7 +2619,6 @@ The Symfony Connect username in parenthesis allows to get more information - tpetry - JustDylan23 - Juraj Surman - - ywisax - Martin Eckhardt - natechicago - Victor @@ -2746,6 +2765,7 @@ The Symfony Connect username in parenthesis allows to get more information - botbotbot - tatankat - Cláudio Cesar + - Sven Nolting - Timon van der Vorm - nuncanada - Thierry Marianne @@ -3158,6 +3178,7 @@ The Symfony Connect username in parenthesis allows to get more information - Vlad Dumitrache - wetternest - Erik van Wingerden + - matlec - Valouleloup - Pathpat - Jaymin G @@ -3302,6 +3323,7 @@ The Symfony Connect username in parenthesis allows to get more information - dasmfm - Claas Augner - Mathias Geat + - neodevcode - Angel Fernando Quiroz Campos (angelfqc) - Arnaud Buathier (arnapou) - Curtis (ccorliss) @@ -3347,6 +3369,7 @@ The Symfony Connect username in parenthesis allows to get more information - jersoe - Brian Debuire - Eric Grimois + - Christian Schiffler - Piers Warmers - Sylvain Lorinet - klyk50 @@ -3393,6 +3416,7 @@ The Symfony Connect username in parenthesis allows to get more information - Menno Holtkamp - Ser5 - Michael Hudson-Doyle + - Matthew Burns - Daniel Bannert - Karim Miladi - Michael Genereux @@ -3438,6 +3462,7 @@ The Symfony Connect username in parenthesis allows to get more information - Kaipi Yann - wiseguy1394 - adam-mospan + - AUDUL - Steve Hyde - AbdelatifAitBara - nerdgod @@ -3771,6 +3796,7 @@ The Symfony Connect username in parenthesis allows to get more information - damaya - Kevin Weber - Alexandru Năstase + - Carl Julian Sauter - Dionysis Arvanitis - Sergey Fedotov - Konstantin Scheumann @@ -3801,7 +3827,6 @@ The Symfony Connect username in parenthesis allows to get more information - Courcier Marvin (helyakin) - Henne Van Och (hennevo) - Jeroen De Dauw (jeroendedauw) - - Maxime COLIN (maximecolin) - Muharrem Demirci (mdemirci) - Evgeny Z (meze) - Aleksandar Dimitrov (netbull) diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php index e4831557f01db..8e10891b0ba74 100644 --- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php @@ -27,15 +27,17 @@ class DoctrineTransactionMiddleware extends AbstractDoctrineMiddleware protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope { $entityManager->getConnection()->beginTransaction(); + + $success = false; try { $envelope = $stack->next()->handle($envelope, $stack); $entityManager->flush(); $entityManager->getConnection()->commit(); + $success = true; + return $envelope; } catch (\Throwable $exception) { - $entityManager->getConnection()->rollBack(); - if ($exception instanceof HandlerFailedException) { // Remove all HandledStamp from the envelope so the retry will execute all handlers again. // When a handler fails, the queries of allegedly successful previous handlers just got rolled back. @@ -43,6 +45,12 @@ protected function handleForManager(EntityManagerInterface $entityManager, Envel } throw $exception; + } finally { + $connection = $entityManager->getConnection(); + + if (!$success && $connection->isTransactionActive()) { + $connection->rollBack(); + } } } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php index 977f32e30fa61..05e5dae1b34ac 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php @@ -56,12 +56,9 @@ public function testMiddlewareWrapsInTransactionAndFlushes() public function testTransactionIsRolledBackOnException() { - $this->connection->expects($this->once()) - ->method('beginTransaction') - ; - $this->connection->expects($this->once()) - ->method('rollBack') - ; + $this->connection->expects($this->once())->method('beginTransaction'); + $this->connection->expects($this->once())->method('isTransactionActive')->willReturn(true); + $this->connection->expects($this->once())->method('rollBack'); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Thrown from next middleware.'); @@ -69,6 +66,27 @@ public function testTransactionIsRolledBackOnException() $this->middleware->handle(new Envelope(new \stdClass()), $this->getThrowingStackMock()); } + public function testExceptionInRollBackDoesNotHidePreviousException() + { + $this->connection->expects($this->once())->method('beginTransaction'); + $this->connection->expects($this->once())->method('isTransactionActive')->willReturn(true); + $this->connection->expects($this->once())->method('rollBack')->willThrowException(new \RuntimeException('Thrown from rollBack.')); + + try { + $this->middleware->handle(new Envelope(new \stdClass()), $this->getThrowingStackMock()); + } catch (\Throwable $exception) { + } + + self::assertNotNull($exception); + self::assertInstanceOf(\RuntimeException::class, $exception); + self::assertSame('Thrown from rollBack.', $exception->getMessage()); + + $previous = $exception->getPrevious(); + self::assertNotNull($previous); + self::assertInstanceOf(\RuntimeException::class, $previous); + self::assertSame('Thrown from next middleware.', $previous->getMessage()); + } + public function testInvalidEntityManagerThrowsException() { $managerRegistry = $this->createMock(ManagerRegistry::class); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index 9b7eb0b1165c8..a7057fda57d88 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -81,7 +81,7 @@ public function testGenerateFragmentUri() ]); $twig->addRuntimeLoader($loader); - $this->assertSame('/_fragment?_hash=XCg0hX8QzSwik8Xuu9aMXhoCeI4oJOob7lUVacyOtyY%3D&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CController%255CTemplateController%253A%253AtemplateAction', $twig->render('index')); + $this->assertMatchesRegularExpression('#/_fragment\?_hash=.+&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CController%255CTemplateController%253A%253AtemplateAction$#', $twig->render('index')); } protected function getFragmentHandler($returnOrException): FragmentHandler diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php index 96f707cdfdf2c..f6dd5f623baee 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php @@ -206,6 +206,68 @@ public function testDefaultTranslationDomainWithNamedArguments() $this->assertEquals('foo (custom)foo (foo)foo (custom)foo (custom)foo (fr)foo (custom)foo (fr)', trim($template->render([]))); } + public function testDefaultTranslationDomainWithExpression() + { + $templates = [ + 'index' => ' + {%- extends "base" %} + + {%- trans_default_domain custom_domain %} + + {%- block content %} + {{- "foo"|trans }} + {%- endblock %} + ', + + 'base' => ' + {%- block content "" %} + ', + ]; + + $translator = new Translator('en'); + $translator->addLoader('array', new ArrayLoader()); + $translator->addResource('array', ['foo' => 'foo (messages)'], 'en'); + $translator->addResource('array', ['foo' => 'foo (custom)'], 'en', 'custom'); + $translator->addResource('array', ['foo' => 'foo (foo)'], 'en', 'foo'); + + $template = $this->getTemplate($templates, $translator); + + $this->assertEquals('foo (foo)', trim($template->render(['custom_domain' => 'foo']))); + } + + public function testDefaultTranslationDomainWithExpressionAndInheritance() + { + $templates = [ + 'index' => ' + {%- extends "base" %} + + {%- trans_default_domain foo_domain %} + + {%- block content %} + {{- "foo"|trans }} + {%- endblock %} + ', + + 'base' => ' + {%- trans_default_domain custom_domain %} + + {{- "foo"|trans }} + {%- block content "" %} + {{- "foo"|trans }} + ', + ]; + + $translator = new Translator('en'); + $translator->addLoader('array', new ArrayLoader()); + $translator->addResource('array', ['foo' => 'foo (messages)'], 'en'); + $translator->addResource('array', ['foo' => 'foo (custom)'], 'en', 'custom'); + $translator->addResource('array', ['foo' => 'foo (foo)'], 'en', 'foo'); + + $template = $this->getTemplate($templates, $translator); + + $this->assertEquals('foo (custom)foo (foo)foo (custom)', trim($template->render(['foo_domain' => 'foo', 'custom_domain' => 'custom']))); + } + private function getTemplate($template, ?TranslatorInterface $translator = null): TemplateWrapper { $translator ??= new Translator('en'); diff --git a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php index 5c2bacf19d5f8..47ec58acb36cb 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php @@ -22,6 +22,7 @@ use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; use Twig\Node\Nodes; @@ -308,32 +309,32 @@ public function testCompileLabelWithLabelAndAttributes() public function testCompileLabelWithLabelThatEvaluatesToNull() { + if (class_exists(ConditionalTernary::class)) { + $conditional = new ConditionalTernary( + // if + new ConstantExpression(true, 0), + // then + new ConstantExpression(null, 0), + // else + new ConstantExpression(null, 0), + 0 + ); + } else { + $conditional = new ConditionalExpression( + // if + new ConstantExpression(true, 0), + // then + new ConstantExpression(null, 0), + // else + new ConstantExpression(null, 0), + 0 + ); + } + if (class_exists(Nodes::class)) { - $arguments = new Nodes([ - new ContextVariable('form', 0), - new ConditionalExpression( - // if - new ConstantExpression(true, 0), - // then - new ConstantExpression(null, 0), - // else - new ConstantExpression(null, 0), - 0 - ), - ]); + $arguments = new Nodes([new ContextVariable('form', 0), $conditional]); } else { - $arguments = new Node([ - new NameExpression('form', 0), - new ConditionalExpression( - // if - new ConstantExpression(true, 0), - // then - new ConstantExpression(null, 0), - // else - new ConstantExpression(null, 0), - 0 - ), - ]); + $arguments = new Node([new NameExpression('form', 0), $conditional]); } if (class_exists(FirstClassTwigCallableReady::class)) { @@ -359,18 +360,32 @@ public function testCompileLabelWithLabelThatEvaluatesToNull() public function testCompileLabelWithLabelThatEvaluatesToNullAndAttributes() { + if (class_exists(ConditionalTernary::class)) { + $conditional = new ConditionalTernary( + // if + new ConstantExpression(true, 0), + // then + new ConstantExpression(null, 0), + // else + new ConstantExpression(null, 0), + 0 + ); + } else { + $conditional = new ConditionalExpression( + // if + new ConstantExpression(true, 0), + // then + new ConstantExpression(null, 0), + // else + new ConstantExpression(null, 0), + 0 + ); + } + if (class_exists(Nodes::class)) { $arguments = new Nodes([ new ContextVariable('form', 0), - new ConditionalExpression( - // if - new ConstantExpression(true, 0), - // then - new ConstantExpression(null, 0), - // else - new ConstantExpression(null, 0), - 0 - ), + $conditional, new ArrayExpression([ new ConstantExpression('foo', 0), new ConstantExpression('bar', 0), @@ -381,12 +396,7 @@ public function testCompileLabelWithLabelThatEvaluatesToNullAndAttributes() } else { $arguments = new Node([ new NameExpression('form', 0), - new ConditionalExpression( - new ConstantExpression(true, 0), - new ConstantExpression(null, 0), - new ConstantExpression(null, 0), - 0 - ), + $conditional, new ArrayExpression([ new ConstantExpression('foo', 0), new ConstantExpression('bar', 0), diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index 91e7e33491d97..0ffe6a949d472 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -64,6 +64,10 @@ public function __construct(TranslationWriterInterface $writer, TranslationReade { parent::__construct(); + if (!method_exists($writer, 'getFormats')) { + throw new \InvalidArgumentException(sprintf('The writer class "%s" does not implement the "getFormats()" method.', $writer::class)); + } + $this->writer = $writer; $this->reader = $reader; $this->extractor = $extractor; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php new file mode 100644 index 0000000000000..7542191d0e83e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class TranslationUpdateCommandPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('console.command.translation_extract')) { + return; + } + + $translationWriterClass = $container->getParameterBag()->resolveValue($container->findDefinition('translation.writer')->getClass()); + + if (!method_exists($translationWriterClass, 'getFormats')) { + $container->removeDefinition('console.command.translation_extract'); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 83518061fed36..aed6bdefa2c6f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -122,6 +122,8 @@ use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Notifier\Bridge as NotifierBridge; +use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory; +use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory; use Symfony\Component\Notifier\ChatterInterface; use Symfony\Component\Notifier\Notifier; use Symfony\Component\Notifier\Recipient\Recipient; @@ -2765,7 +2767,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $container->removeDefinition('notifier.channel.email'); } - foreach (['texter', 'chatter', 'notifier.channel.chat', 'notifier.channel.email', 'notifier.channel.sms'] as $serviceId) { + foreach (['texter', 'chatter', 'notifier.channel.chat', 'notifier.channel.email', 'notifier.channel.sms', 'notifier.channel.push'] as $serviceId) { if (!$container->hasDefinition($serviceId)) { continue; } @@ -2812,8 +2814,6 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\Engagespot\EngagespotTransportFactory::class => 'notifier.transport_factory.engagespot', NotifierBridge\Esendex\EsendexTransportFactory::class => 'notifier.transport_factory.esendex', NotifierBridge\Expo\ExpoTransportFactory::class => 'notifier.transport_factory.expo', - NotifierBridge\FakeChat\FakeChatTransportFactory::class => 'notifier.transport_factory.fake-chat', - NotifierBridge\FakeSms\FakeSmsTransportFactory::class => 'notifier.transport_factory.fake-sms', NotifierBridge\Firebase\FirebaseTransportFactory::class => 'notifier.transport_factory.firebase', NotifierBridge\FortySixElks\FortySixElksTransportFactory::class => 'notifier.transport_factory.forty-six-elks', NotifierBridge\FreeMobile\FreeMobileTransportFactory::class => 'notifier.transport_factory.free-mobile', @@ -2891,20 +2891,26 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $container->removeDefinition($classToServices[NotifierBridge\Mercure\MercureTransportFactory::class]); } - if (ContainerBuilder::willBeAvailable('symfony/fake-chat-notifier', NotifierBridge\FakeChat\FakeChatTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) { - $container->getDefinition($classToServices[NotifierBridge\FakeChat\FakeChatTransportFactory::class]) - ->replaceArgument(0, new Reference('mailer')) - ->replaceArgument(1, new Reference('logger')) + // don't use ContainerBuilder::willBeAvailable() as these are not needed in production + if (class_exists(FakeChatTransportFactory::class)) { + $container->getDefinition('notifier.transport_factory.fake-chat') + ->replaceArgument(0, new Reference('mailer', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) + ->replaceArgument(1, new Reference('logger', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) ->addArgument(new Reference('event_dispatcher', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) ->addArgument(new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE)); + } else { + $container->removeDefinition('notifier.transport_factory.fake-chat'); } - if (ContainerBuilder::willBeAvailable('symfony/fake-sms-notifier', NotifierBridge\FakeSms\FakeSmsTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) { - $container->getDefinition($classToServices[NotifierBridge\FakeSms\FakeSmsTransportFactory::class]) - ->replaceArgument(0, new Reference('mailer')) - ->replaceArgument(1, new Reference('logger')) + // don't use ContainerBuilder::willBeAvailable() as these are not needed in production + if (class_exists(FakeSmsTransportFactory::class)) { + $container->getDefinition('notifier.transport_factory.fake-sms') + ->replaceArgument(0, new Reference('mailer', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) + ->replaceArgument(1, new Reference('logger', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) ->addArgument(new Reference('event_dispatcher', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) ->addArgument(new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE)); + } else { + $container->removeDefinition('notifier.transport_factory.fake-sms'); } if (isset($config['admin_recipients'])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 0da10da9d77f8..c371d10dbc684 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -20,6 +20,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationUpdateCommandPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\VirtualRequestStackPass; use Symfony\Component\Cache\Adapter\ApcuAdapter; @@ -193,6 +194,7 @@ public function build(ContainerBuilder $container) // must be registered after MonologBundle's LoggerChannelPass $container->addCompilerPass(new ErrorLoggerCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new VirtualRequestStackPass()); + $container->addCompilerPass(new TranslationUpdateCommandPass(), PassConfig::TYPE_BEFORE_REMOVING); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php index 6ce674148a878..bcc1248208c61 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php @@ -73,7 +73,10 @@ ->tag('notifier.channel', ['channel' => 'email']) ->set('notifier.channel.push', PushChannel::class) - ->args([service('texter.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->args([ + service('texter.transports'), + abstract_arg('message bus'), + ]) ->tag('notifier.channel', ['channel' => 'push']) ->set('notifier.monolog_handler', NotifierHandler::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php index 6d8966a171ba2..48d5c327a3986 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php @@ -50,6 +50,6 @@ public function testGenerateFragmentUri() $client = self::createClient(['test_case' => 'Fragment', 'root_config' => 'config.yml', 'debug' => true]); $client->request('GET', '/fragment_uri'); - $this->assertSame('/_fragment?_hash=CCRGN2D%2FoAJbeGz%2F%2FdoH3bNSPwLCrmwC1zAYCGIKJ0E%3D&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction', $client->getResponse()->getContent()); + $this->assertMatchesRegularExpression('#/_fragment\?_hash=.+&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction$#', $client->getResponse()->getContent()); } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig index af9f0a4ceaba3..55589c2945d88 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig @@ -40,6 +40,7 @@ #source .source-content ol li { margin: 0 0 2px 0; padding-left: 5px; + white-space: preserve nowrap; } #source .source-content ol li::marker { color: var(--color-muted); diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index b406292b44b3a..23157e3c7b2db 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -229,7 +229,7 @@ public function getEstimated(): float public function getRemaining(): float { - if (!$this->step) { + if (0 === $this->step || $this->step === $this->startingStep) { return 0; } diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index a5a0eca245080..a1db94583db49 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -110,6 +110,16 @@ public function testRegularTimeEstimation() ); } + public function testRegularTimeRemainingWithDifferentStartAtAndCustomDisplay() + { + $this->expectNotToPerformAssertions(); + + ProgressBar::setFormatDefinition('custom', ' %current%/%max% [%bar%] %percent:3s%% %remaining% %estimated%'); + $bar = new ProgressBar($this->getOutputStream(), 1_200, 0); + $bar->setFormat('custom'); + $bar->start(1_200, 600); + } + public function testResumedTimeEstimation() { $bar = new ProgressBar($output = $this->getOutputStream(), 1_200, 0); diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt index cfb10d03dafdd..81becafd8e350 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt @@ -24,7 +24,7 @@ var_dump([ $eHandler[0]->setExceptionHandler('print_r'); if (true) { - class Broken implements \JsonSerializable + class Broken implements \Iterator { } } @@ -37,14 +37,14 @@ array(1) { } object(Symfony\Component\ErrorHandler\Error\FatalError)#%d (%d) { ["message":protected]=> - string(186) "Error: Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" + string(209) "Error: Class Symfony\Component\ErrorHandler\Broken contains 5 abstract methods and must therefore be declared abstract or implement the remaining methods (Iterator::current, Iterator::next, Iterator::key, ...)" %a ["error":"Symfony\Component\ErrorHandler\Error\FatalError":private]=> array(4) { ["type"]=> int(1) ["message"]=> - string(179) "Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" + string(202) "Class Symfony\Component\ErrorHandler\Broken contains 5 abstract methods and must therefore be declared abstract or implement the remaining methods (Iterator::current, Iterator::next, Iterator::key, ...)" ["file"]=> string(%d) "%s" ["line"]=> diff --git a/src/Symfony/Component/Finder/Comparator/DateComparator.php b/src/Symfony/Component/Finder/Comparator/DateComparator.php index e0c523d05523b..f7c27de677fb1 100644 --- a/src/Symfony/Component/Finder/Comparator/DateComparator.php +++ b/src/Symfony/Component/Finder/Comparator/DateComparator.php @@ -36,7 +36,7 @@ public function __construct(string $test) throw new \InvalidArgumentException(sprintf('"%s" is not a valid date.', $matches[2])); } - $operator = $matches[1] ?? '=='; + $operator = $matches[1] ?: '=='; if ('since' === $operator || 'after' === $operator) { $operator = '>'; } diff --git a/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php b/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php index 47bcc4838bd26..e50b713062638 100644 --- a/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php +++ b/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php @@ -59,6 +59,7 @@ public static function getTestData() ['after 2005-10-10', [strtotime('2005-10-15')], [strtotime('2005-10-09')]], ['since 2005-10-10', [strtotime('2005-10-15')], [strtotime('2005-10-09')]], ['!= 2005-10-10', [strtotime('2005-10-11')], [strtotime('2005-10-10')]], + ['2005-10-10', [strtotime('2005-10-10')], [strtotime('2005-10-11')]], ]; } } diff --git a/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php b/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php index fe0e0d39cd9d9..c00b8f7dfbfe5 100644 --- a/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php +++ b/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php @@ -358,10 +358,10 @@ public static function provideParse(): iterable 'non-special://:@untrusted.com/x' => ['scheme' => 'non-special', 'host' => 'untrusted.com'], 'http:foo.com' => ['scheme' => 'http', 'host' => null], " :foo.com \n" => null, - ' foo.com ' => ['scheme' => null, 'host' => null], + ' foo.com ' => null, 'a: foo.com' => null, - 'http://f:21/ b ? d # e ' => ['scheme' => 'http', 'host' => 'f'], - 'lolscheme:x x#x x' => ['scheme' => 'lolscheme', 'host' => null], + 'http://f:21/ b ? d # e ' => null, + 'lolscheme:x x#x x' => null, 'http://f:/c' => ['scheme' => 'http', 'host' => 'f'], 'http://f:0/c' => ['scheme' => 'http', 'host' => 'f'], 'http://f:00000000000000/c' => ['scheme' => 'http', 'host' => 'f'], @@ -434,7 +434,7 @@ public static function provideParse(): iterable 'javascript:example.com/' => ['scheme' => 'javascript', 'host' => null], 'mailto:example.com/' => ['scheme' => 'mailto', 'host' => null], '/a/b/c' => ['scheme' => null, 'host' => null], - '/a/ /c' => ['scheme' => null, 'host' => null], + '/a/ /c' => null, '/a%2fc' => ['scheme' => null, 'host' => null], '/a/%2f/c' => ['scheme' => null, 'host' => null], '#β' => ['scheme' => null, 'host' => null], @@ -495,10 +495,10 @@ public static function provideParse(): iterable 'http://example.com/你好你好' => ['scheme' => 'http', 'host' => 'example.com'], 'http://example.com/‥/foo' => ['scheme' => 'http', 'host' => 'example.com'], "http://example.com/\u{feff}/foo" => ['scheme' => 'http', 'host' => 'example.com'], - "http://example.com\u{002f}\u{202e}\u{002f}\u{0066}\u{006f}\u{006f}\u{002f}\u{202d}\u{002f}\u{0062}\u{0061}\u{0072}\u{0027}\u{0020}" => ['scheme' => 'http', 'host' => 'example.com'], + "http://example.com\u{002f}\u{202e}\u{002f}\u{0066}\u{006f}\u{006f}\u{002f}\u{202d}\u{002f}\u{0062}\u{0061}\u{0072}\u{0027}\u{0020}" => null, 'http://www.google.com/foo?bar=baz#' => ['scheme' => 'http', 'host' => 'www.google.com'], - 'http://www.google.com/foo?bar=baz# »' => ['scheme' => 'http', 'host' => 'www.google.com'], - 'data:test# »' => ['scheme' => 'data', 'host' => null], + 'http://www.google.com/foo?bar=baz# »' => null, + 'data:test# »' => null, 'http://www.google.com' => ['scheme' => 'http', 'host' => 'www.google.com'], 'http://192.0x00A80001' => ['scheme' => 'http', 'host' => '192.0x00A80001'], 'http://www/foo%2Ehtml' => ['scheme' => 'http', 'host' => 'www'], @@ -706,11 +706,11 @@ public static function provideParse(): iterable 'test-a-colon-slash-slash-b.html' => ['scheme' => null, 'host' => null], 'http://example.org/test?a#bc' => ['scheme' => 'http', 'host' => 'example.org'], 'http:\\/\\/f:b\\/c' => ['scheme' => 'http', 'host' => null], - 'http:\\/\\/f: \\/c' => ['scheme' => 'http', 'host' => null], + 'http:\\/\\/f: \\/c' => null, 'http:\\/\\/f:fifty-two\\/c' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/f:999999\\/c' => ['scheme' => 'http', 'host' => null], 'non-special:\\/\\/f:999999\\/c' => ['scheme' => 'non-special', 'host' => null], - 'http:\\/\\/f: 21 \\/ b ? d # e ' => ['scheme' => 'http', 'host' => null], + 'http:\\/\\/f: 21 \\/ b ? d # e ' => null, 'http:\\/\\/[1::2]:3:4' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/2001::1' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/2001::1]' => ['scheme' => 'http', 'host' => null], @@ -734,8 +734,8 @@ public static function provideParse(): iterable 'http:@:www.example.com' => ['scheme' => 'http', 'host' => null], 'http:\\/@:www.example.com' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/@:www.example.com' => ['scheme' => 'http', 'host' => null], - 'http:\\/\\/example example.com' => ['scheme' => 'http', 'host' => null], - 'http:\\/\\/Goo%20 goo%7C|.com' => ['scheme' => 'http', 'host' => null], + 'http:\\/\\/example example.com' => null, + 'http:\\/\\/Goo%20 goo%7C|.com' => null, 'http:\\/\\/[]' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/[:]' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/GOO\\u00a0\\u3000goo.com' => ['scheme' => 'http', 'host' => null], @@ -752,8 +752,8 @@ public static function provideParse(): iterable 'http:\\/\\/hello%00' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/192.168.0.257' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/%3g%78%63%30%2e%30%32%35%30%2E.01' => ['scheme' => 'http', 'host' => null], - 'http:\\/\\/192.168.0.1 hello' => ['scheme' => 'http', 'host' => null], - 'https:\\/\\/x x:12' => ['scheme' => 'https', 'host' => null], + 'http:\\/\\/192.168.0.1 hello' => null, + 'https:\\/\\/x x:12' => null, 'http:\\/\\/[www.google.com]\\/' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/[google.com]' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/[::1.2.3.4x]' => ['scheme' => 'http', 'host' => null], @@ -763,7 +763,7 @@ public static function provideParse(): iterable '..\\/i' => ['scheme' => null, 'host' => null], '\\/i' => ['scheme' => null, 'host' => null], 'sc:\\/\\/\\u0000\\/' => ['scheme' => 'sc', 'host' => null], - 'sc:\\/\\/ \\/' => ['scheme' => 'sc', 'host' => null], + 'sc:\\/\\/ \\/' => null, 'sc:\\/\\/@\\/' => ['scheme' => 'sc', 'host' => null], 'sc:\\/\\/te@s:t@\\/' => ['scheme' => 'sc', 'host' => null], 'sc:\\/\\/:\\/' => ['scheme' => 'sc', 'host' => null], diff --git a/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php b/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php index a806981de770f..05d86ba15da8e 100644 --- a/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php +++ b/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php @@ -94,7 +94,13 @@ public static function parse(string $url): ?array } try { - return UriString::parse($url); + $parsedUrl = UriString::parse($url); + + if (preg_match('/\s/', $url)) { + return null; + } + + return $parsedUrl; } catch (SyntaxError) { return null; } diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 7d996200527eb..3e15bef74cc9e 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -197,13 +197,12 @@ public function request(string $method, string $url, array $options = []): Respo $curlopts[\CURLOPT_RESOLVE] = $resolve; } + $curlopts[\CURLOPT_CUSTOMREQUEST] = $method; if ('POST' === $method) { // Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303 $curlopts[\CURLOPT_POST] = true; } elseif ('HEAD' === $method) { $curlopts[\CURLOPT_NOBODY] = true; - } else { - $curlopts[\CURLOPT_CUSTOMREQUEST] = $method; } if ('\\' !== \DIRECTORY_SEPARATOR && $options['timeout'] < 1) { diff --git a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php index b38b0178b90f2..4094f98806323 100644 --- a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php +++ b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php @@ -20,7 +20,6 @@ use Symfony\Contracts\HttpClient\ChunkInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; -use Symfony\Contracts\HttpClient\ResponseStreamInterface; use Symfony\Contracts\Service\ResetInterface; /** @@ -81,17 +80,6 @@ public function request(string $method, string $url, array $options = []): Respo $ip = self::dnsResolve($dnsCache, $host, $this->ipFlags, $options); self::ipCheck($ip, $this->subnets, $this->ipFlags, $host, $url); - if (0 < $maxRedirects = $options['max_redirects']) { - $options['max_redirects'] = 0; - $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = $options['headers']; - - if (isset($options['normalized_headers']['host']) || isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) { - $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) { - return 0 !== stripos($h, 'Host:') && 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:'); - }); - } - } - $onProgress = $options['on_progress'] ?? null; $subnets = $this->subnets; $ipFlags = $this->ipFlags; @@ -99,7 +87,7 @@ public function request(string $method, string $url, array $options = []): Respo $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, $ipFlags): void { static $lastPrimaryIp = ''; - if (($info['primary_ip'] ?? '') !== $lastPrimaryIp) { + if (!\in_array($info['primary_ip'] ?? '', ['', $lastPrimaryIp], true)) { self::ipCheck($info['primary_ip'], $subnets, $ipFlags, null, $info['url']); $lastPrimaryIp = $info['primary_ip']; } @@ -107,6 +95,19 @@ public function request(string $method, string $url, array $options = []): Respo null !== $onProgress && $onProgress($dlNow, $dlSize, $info); }; + if (0 >= $maxRedirects = $options['max_redirects']) { + return new AsyncResponse($this->client, $method, $url, $options); + } + + $options['max_redirects'] = 0; + $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = $options['headers']; + + if (isset($options['normalized_headers']['host']) || isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) { + $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) { + return 0 !== stripos($h, 'Host:') && 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:'); + }); + } + return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use (&$method, &$options, $maxRedirects, &$redirectHeaders, $subnets, $ipFlags, $dnsCache): \Generator { if (null !== $chunk->getError() || $chunk->isTimeout() || !$chunk->isFirst()) { yield $chunk; @@ -137,7 +138,7 @@ public function request(string $method, string $url, array $options = []): Respo $filterContentHeaders = static function ($h) { return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:'); }; - $options['header'] = array_filter($options['header'], $filterContentHeaders); + $options['headers'] = array_filter($options['headers'], $filterContentHeaders); $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders); $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders); } @@ -158,11 +159,6 @@ public function request(string $method, string $url, array $options = []): Respo }); } - public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface - { - return $this->client->stream($responses, $timeout); - } - public function setLogger(LoggerInterface $logger): void { if ($this->client instanceof LoggerAwareInterface) { diff --git a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php index 91ec7ea5f2c7c..a7493100c431d 100644 --- a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php +++ b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php @@ -24,11 +24,6 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } - public static function tearDownAfterClass(): void - { - TestHttpServer::stop(); - } - public function testItCollectsRequestCount() { $httpClient1 = $this->httpClientThatHasTracedRequests([ diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index 3385323f75c67..79763bc1019f3 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -518,6 +518,73 @@ public function testNoPrivateNetworkWithResolveAndRedirect() $client->request('GET', 'http://localhost:8057/302?location=https://symfony.com/'); } + public function testNoPrivateNetwork304() + { + $client = $this->getHttpClient(__FUNCTION__); + $client = new NoPrivateNetworkHttpClient($client, '104.26.14.6/32'); + $response = $client->request('GET', 'http://localhost:8057/304', [ + 'headers' => ['If-Match' => '"abc"'], + 'buffer' => false, + ]); + + $this->assertSame(304, $response->getStatusCode()); + $this->assertSame('', $response->getContent(false)); + } + + public function testNoPrivateNetwork302() + { + $client = $this->getHttpClient(__FUNCTION__); + $client = new NoPrivateNetworkHttpClient($client, '104.26.14.6/32'); + $response = $client->request('GET', 'http://localhost:8057/302/relative'); + + $body = $response->toArray(); + + $this->assertSame('/', $body['REQUEST_URI']); + $this->assertNull($response->getInfo('redirect_url')); + + $response = $client->request('GET', 'http://localhost:8057/302/relative', [ + 'max_redirects' => 0, + ]); + + $this->assertSame(302, $response->getStatusCode()); + $this->assertSame('http://localhost:8057/', $response->getInfo('redirect_url')); + } + + public function testNoPrivateNetworkStream() + { + $client = $this->getHttpClient(__FUNCTION__); + + $response = $client->request('GET', 'http://localhost:8057'); + $client = new NoPrivateNetworkHttpClient($client, '104.26.14.6/32'); + + $response = $client->request('GET', 'http://localhost:8057'); + $chunks = $client->stream($response); + $result = []; + + foreach ($chunks as $r => $chunk) { + if ($chunk->isTimeout()) { + $result[] = 't'; + } elseif ($chunk->isLast()) { + $result[] = 'l'; + } elseif ($chunk->isFirst()) { + $result[] = 'f'; + } + } + + $this->assertSame($response, $r); + $this->assertSame(['f', 'l'], $result); + + $chunk = null; + $i = 0; + + foreach ($client->stream($response) as $chunk) { + ++$i; + } + + $this->assertSame(1, $i); + $this->assertTrue($chunk->isLast()); + } + public function testNoRedirectWithInvalidLocation() { $client = $this->getHttpClient(__FUNCTION__); @@ -584,4 +651,48 @@ public function testDefaultContentType() $this->assertSame(['abc' => 'def', 'content-type' => 'application/json', 'REQUEST_METHOD' => 'POST'], $response->toArray()); } + + public function testHeadRequestWithClosureBody() + { + $p = TestHttpServer::start(8067); + + try { + $client = $this->getHttpClient(__FUNCTION__); + + $response = $client->request('HEAD', 'http://localhost:8057/head', [ + 'body' => fn () => '', + ]); + $headers = $response->getHeaders(); + } finally { + $p->stop(); + } + + $this->assertArrayHasKey('x-request-vars', $headers); + + $vars = json_decode($headers['x-request-vars'][0], true); + $this->assertIsArray($vars); + $this->assertSame('HEAD', $vars['REQUEST_METHOD']); + } + + /** + * @testWith [301] + * [302] + * [303] + */ + public function testPostToGetRedirect(int $status) + { + $p = TestHttpServer::start(8067); + + try { + $client = $this->getHttpClient(__FUNCTION__); + + $response = $client->request('POST', 'http://localhost:8057/custom?status=' . $status . '&headers[]=Location%3A%20%2F'); + $body = $response->toArray(); + } finally { + $p->stop(); + } + + $this->assertSame('GET', $body['REQUEST_METHOD']); + $this->assertSame('/', $body['REQUEST_URI']); + } } diff --git a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php index f5f6d8ddcf9be..b500c9548ebb0 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php @@ -32,11 +32,6 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } - public static function tearDownAfterClass(): void - { - TestHttpServer::stop(); - } - /** * @requires function ob_gzhandler */ diff --git a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php index fb940790b0b3f..06ffc128187cf 100644 --- a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php @@ -173,6 +173,27 @@ public function testNonCallableOnProgressCallback() $client->request('GET', $url, ['on_progress' => $customCallback]); } + public function testHeadersArePassedOnRedirect() + { + $ipAddr = '104.26.14.6'; + $url = sprintf('http://%s/', $ipAddr); + $content = 'foo'; + + $callback = function ($method, $url, $options) use ($content): MockResponse { + $this->assertArrayHasKey('headers', $options); + $this->assertNotContains('content-type: application/json', $options['headers']); + $this->assertContains('foo: bar', $options['headers']); + return new MockResponse($content); + }; + $responses = [ + new MockResponse('', ['http_code' => 302, 'redirect_url' => 'http://104.26.14.7']), + $callback, + ]; + $client = new NoPrivateNetworkHttpClient(new MockHttpClient($responses)); + $response = $client->request('POST', $url, ['headers' => ['foo' => 'bar', 'content-type' => 'application/json']]); + $this->assertEquals($content, $response->getContent()); + } + private function getMockHttpClient(string $ipAddr, string $content) { return new MockHttpClient(new MockResponse($content, ['primary_ip' => $ipAddr])); diff --git a/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php b/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php index 65b7f5b3f6794..bf49535ae3e66 100644 --- a/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php @@ -28,11 +28,6 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } - public static function tearDownAfterClass(): void - { - TestHttpServer::stop(); - } - /** * @requires function ob_gzhandler */ diff --git a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php index a0e39cc46c851..ba9504ae1c66d 100644 --- a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php @@ -27,11 +27,6 @@ class RetryableHttpClientTest extends TestCase { - public static function tearDownAfterClass(): void - { - TestHttpServer::stop(); - } - public function testRetryOnError() { $client = new RetryableHttpClient( diff --git a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php index b6a2c03c8f7a3..cf437a653bd76 100644 --- a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php @@ -29,11 +29,6 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } - public static function tearDownAfterClass(): void - { - TestHttpServer::stop(); - } - public function testItTracesRequest() { $httpClient = $this->createMock(HttpClientInterface::class); diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json index 23437d5363f28..9c9ee14a4a3ff 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -25,7 +25,7 @@ "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "~3.4.3|^3.5.1", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, "require-dev": { diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php index b104f861f92ee..f0f735da42524 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -40,11 +40,9 @@ class RequestPayloadValueResolver implements ValueResolverInterface, EventSubscriberInterface { /** - * @see \Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT * @see DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS */ private const CONTEXT_DENORMALIZE = [ - 'disable_type_enforcement' => true, 'collect_denormalization_errors' => true, ]; @@ -165,7 +163,7 @@ private function mapQueryString(Request $request, string $type, MapQueryString $ return null; } - return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE); + return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE); } private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): ?object @@ -179,7 +177,7 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay } if ($data = $request->request->all()) { - return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE); + return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE); } if ('' === $data = $request->getContent()) { diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index e4d06fad61928..eb98942076da3 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -76,11 +76,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.4.16'; - public const VERSION_ID = 60416; + public const VERSION = '6.4.17'; + public const VERSION_ID = 60417; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 16; + public const RELEASE_VERSION = 17; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2026'; diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 9b68c80ff27e5..1b4c20672ab46 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -22,6 +22,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -394,6 +395,38 @@ public function testQueryStringValidationPassed() $this->assertEquals([$payload], $event->getArguments()); } + public function testQueryStringParameterTypeMismatch() + { + $query = ['price' => 'not a float']; + + $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); + $serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]); + + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->never())->method('validate'); + + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ + MapQueryString::class => new MapQueryString(), + ]); + + $request = Request::create('/', 'GET', $query); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + try { + $resolver->onKernelControllerArguments($event); + $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class)); + } catch (HttpException $e) { + $validationFailedException = $e->getPrevious(); + $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); + $this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage()); + } + } + public function testRequestInputValidationPassed() { $input = ['price' => '50']; @@ -422,6 +455,38 @@ public function testRequestInputValidationPassed() $this->assertEquals([$payload], $event->getArguments()); } + public function testRequestInputTypeMismatch() + { + $input = ['price' => 'not a float']; + + $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); + $serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]); + + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->never())->method('validate'); + + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(), + ]); + + $request = Request::create('/', 'POST', $input); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + try { + $resolver->onKernelControllerArguments($event); + $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class)); + } catch (HttpException $e) { + $validationFailedException = $e->getPrevious(); + $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); + $this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage()); + } + } + public function testItThrowsOnVariadicArgument() { $serializer = new Serializer(); diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php index fa9885d2753cd..43c740ee12b98 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php @@ -60,8 +60,8 @@ public function testRenderControllerReference() $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); - $this->assertEquals( - '', + $this->assertMatchesRegularExpression( + '#^$#', $strategy->render($reference, $request, ['alt' => $altReference])->getContent() ); } @@ -78,8 +78,8 @@ public function testRenderControllerReferenceWithAbsoluteUri() $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); - $this->assertSame( - '', + $this->assertMatchesRegularExpression( + '#^$#', $strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent() ); } diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php index f74887ade36f4..8e4b59e5feeb9 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php @@ -32,7 +32,7 @@ public function testRenderWithControllerAndSigner() { $strategy = new HIncludeFragmentRenderer(null, new UriSigner('foo')); - $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent()); + $this->assertMatchesRegularExpression('#^$#', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent()); } public function testRenderWithUri() diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php index 4af00f9f75137..7fd04c5a5b0b7 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php @@ -51,8 +51,8 @@ public function testRenderControllerReference() $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); - $this->assertEquals( - '', + $this->assertMatchesRegularExpression( + '{^$}', $strategy->render($reference, $request, ['alt' => $altReference])->getContent() ); } @@ -69,8 +69,8 @@ public function testRenderControllerReferenceWithAbsoluteUri() $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); - $this->assertSame( - '', + $this->assertMatchesRegularExpression( + '{^$}', $strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent() ); } diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php index f4cc745846584..bf54ac1272483 100644 --- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php @@ -330,4 +330,25 @@ public function testSendWhenABeanstalkdExceptionOccurs() $connection->send($body, $headers, $delay); } + + public function testSendWithRoundedDelay() + { + $tube = 'xyz'; + $body = 'foo'; + $headers = ['test' => 'bar']; + $delay = 920; + $expectedDelay = 0; + + $client = $this->createMock(PheanstalkInterface::class); + $client->expects($this->once())->method('useTube')->with($tube)->willReturn($client); + $client->expects($this->once())->method('put')->with( + $this->anything(), + $this->anything(), + $expectedDelay, + $this->anything(), + ); + + $connection = new Connection(['tube_name' => $tube], $client); + $connection->send($body, $headers, $delay); + } } diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php index 98ab37c875d18..ff520ddb3071f 100644 --- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php @@ -123,7 +123,7 @@ public function send(string $body, array $headers, int $delay = 0): string $job = $this->client->useTube($this->tube)->put( $message, PheanstalkInterface::DEFAULT_PRIORITY, - $delay / 1000, + (int) ($delay / 1000), $this->ttr ); } catch (Exception $exception) { diff --git a/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php b/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php index 904c86d6b3cfd..0db5dfa0abe44 100644 --- a/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php +++ b/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php @@ -26,7 +26,7 @@ final class FormDataPart extends AbstractMultipartPart private array $fields = []; /** - * @param array $fields + * @param array $fields */ public function __construct(array $fields = []) { diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 8e00e6473a4e8..89239a53f3505 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -581,8 +581,12 @@ private function isAllowedProperty(string $class, string $property, bool $writeA return false; } - if (\PHP_VERSION_ID >= 80400 && ($reflectionProperty->isProtectedSet() || $reflectionProperty->isPrivateSet())) { - return false; + if (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isProtectedSet()) { + return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PROTECTED); + } + + if (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isPrivateSet()) { + return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PRIVATE); } if (\PHP_VERSION_ID >= 80400 &&$reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) { diff --git a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php index 8de070dc046c9..d006e32483896 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php +++ b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php @@ -15,8 +15,6 @@ * The property read info tells how a property can be read. * * @author Joel Wurtz - * - * @internal */ final class PropertyReadInfo { diff --git a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php index 6bc7abcdf849e..81ce7eda6d5b0 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php +++ b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php @@ -15,8 +15,6 @@ * The write mutator defines how a property can be written. * * @author Joel Wurtz - * - * @internal */ final class PropertyWriteInfo { diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index b97032574ab86..b7987668b4f8f 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -14,10 +14,13 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Clazz; use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyCollection; +use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyGeneric; +use Symfony\Component\PropertyInfo\Tests\Fixtures\IFace; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80PromotedDummy; @@ -482,7 +485,88 @@ public static function php80TypesProvider() public function testGenericInterface() { - $this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface')); + $this->assertEquals( + [ + new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: \BackedEnum::class, + collectionValueType: new Type( + builtinType: Type::BUILTIN_TYPE_STRING, + ) + ), + ], + $this->extractor->getTypes(Dummy::class, 'genericInterface') + ); + } + + /** + * @param list $expectedTypes + * @dataProvider genericsProvider + */ + public function testGenericsLegacy(string $property, array $expectedTypes) + { + $this->assertEquals($expectedTypes, $this->extractor->getTypes(DummyGeneric::class, $property)); + } + + /** + * @return iterable}> + */ + public static function genericsProvider(): iterable + { + yield [ + 'basicClass', + [ + new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Clazz::class, + collectionValueType: new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; + yield [ + 'nullableClass', + [ + new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Clazz::class, + nullable: true, + collectionValueType: new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; + yield [ + 'basicInterface', + [ + new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: IFace::class, + collectionValueType: new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; + yield [ + 'nullableInterface', + [ + new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: IFace::class, + nullable: true, + collectionValueType: new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index a6315103a2266..45565096d9963 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -672,6 +672,51 @@ public function testAsymmetricVisibility() $this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate')); } + /** + * @requires PHP 8.4 + */ + public function testAsymmetricVisibilityAllowPublicOnly() + { + $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC); + + $this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate')); + } + + /** + * @requires PHP 8.4 + */ + public function testAsymmetricVisibilityAllowProtectedOnly() + { + $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PROTECTED); + + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate')); + } + + /** + * @requires PHP 8.4 + */ + public function testAsymmetricVisibilityAllowPrivateOnly() + { + $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PRIVATE); + + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate')); + $this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate')); + } + /** * @requires PHP 8.4 */ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php new file mode 100644 index 0000000000000..5863fbfc95450 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +interface IFace {} + +class Clazz {} + +class DummyGeneric +{ + + /** + * @var Clazz + */ + public $basicClass; + + /** + * @var ?Clazz + */ + public $nullableClass; + + /** + * @var IFace + */ + public $basicInterface; + + /** + * @var ?IFace + */ + public $nullableInterface; + +} diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php index 7d439f55660dd..56a6b509172c7 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php @@ -128,7 +128,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array $collection = $mainType->isCollection() || \is_a($mainType->getClassName(), \Traversable::class, true) || \is_a($mainType->getClassName(), \ArrayAccess::class, true); // it's safer to fall back to other extractors if the generic type is too abstract - if (!$collection && !class_exists($mainType->getClassName())) { + if (!$collection && !class_exists($mainType->getClassName()) && !interface_exists($mainType->getClassName(), false)) { return []; } diff --git a/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php b/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php index 22162e792fa24..8f1ce90fee72a 100644 --- a/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php +++ b/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php @@ -108,9 +108,9 @@ public static function provideForToString() /** * @dataProvider providerGetNextRunDates */ - public function testGetNextRunDates(\DateTimeImmutable $from, TriggerInterface $trigger, array $expected, int $count = 0) + public function testGetNextRunDates(\DateTimeImmutable $from, TriggerInterface $trigger, array $expected, int $count) { - $this->assertEquals($expected, $this->getNextRunDates($from, $trigger, $count ?? \count($expected))); + $this->assertEquals($expected, $this->getNextRunDates($from, $trigger, $count)); } public static function providerGetNextRunDates(): iterable diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf index 3977f37433060..485d69add1ee8 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf @@ -444,27 +444,27 @@ This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. - This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + این مقدار بسیار کوتاه است. باید حداقل یک کلمه داشته باشد.|این مقدار بسیار کوتاه است. باید حداقل {{ min }} کلمه داشته باشد. This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. - This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + این مقدار بیش از حد طولانی است. باید فقط یک کلمه باشد.|این مقدار بیش از حد طولانی است. باید حداکثر {{ max }} کلمه داشته باشد. This value does not represent a valid week in the ISO 8601 format. - This value does not represent a valid week in the ISO 8601 format. + این مقدار یک هفته معتبر در قالب ISO 8601 را نشان نمی‌دهد. This value is not a valid week. - This value is not a valid week. + این مقدار یک هفته معتبر نیست. This value should not be before week "{{ min }}". - This value should not be before week "{{ min }}". + این مقدار نباید قبل از هفته "{{ min }}" باشد. This value should not be after week "{{ max }}". - This value should not be after week "{{ max }}". + این مقدار نباید بعد از هفته "{{ max }}" باشد. diff --git a/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php b/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php index a75001785ae2d..399f8bdde17b6 100644 --- a/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php +++ b/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php @@ -42,6 +42,7 @@ exit; case '/head': + header('X-Request-Vars: '.json_encode($vars, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); header('Content-Length: '.strlen($json), true); break; @@ -198,6 +199,16 @@ ]); exit; + + case '/custom': + if (isset($_GET['status'])) { + http_response_code((int) $_GET['status']); + } + if (isset($_GET['headers']) && is_array($_GET['headers'])) { + foreach ($_GET['headers'] as $header) { + header($header); + } + } } header('Content-Type: application/json', true); diff --git a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php index 76b3060770f5c..b150f0ce75acf 100644 --- a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php +++ b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php @@ -36,7 +36,6 @@ public static function tearDownAfterClass(): void { TestHttpServer::stop(8067); TestHttpServer::stop(8077); - TestHttpServer::stop(8087); } abstract protected function getHttpClient(string $testCase): HttpClientInterface;