diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 290e3f20cad0b..b699003177332 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -29,16 +29,17 @@ '@Symfony' => true, '@Symfony:risky' => true, 'protected_to_private' => false, - 'native_constant_invocation' => ['strict' => false], 'no_superfluous_phpdoc_tags' => [ 'remove_inheritdoc' => true, 'allow_unused_params' => true, // for future-ready params, to be replaced with https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7377 ], - 'nullable_type_declaration_for_default_null_value' => true, 'header_comment' => ['header' => $fileHeaderComment], + // TODO: Remove once the "compiler_optimized" set includes "sprintf" + 'native_function_invocation' => ['include' => ['@compiler_optimized', 'sprintf'], 'scope' => 'namespaced', 'strict' => true], + 'nullable_type_declaration' => true, + 'nullable_type_declaration_for_default_null_value' => true, 'modernize_strpos' => true, 'get_class_to_class_keyword' => true, - 'nullable_type_declaration' => true, ]) ->setRiskyAllowed(true) ->setFinder( @@ -47,11 +48,6 @@ ->append([__FILE__]) ->notPath('#/Fixtures/#') ->exclude([ - // directories containing files with content that is autogenerated by `var_export`, which breaks CS in output code - // fixture templates - 'Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom', - // resource templates - 'Symfony/Bundle/FrameworkBundle/Resources/views/Form', // explicit trigger_error tests 'Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/', 'Symfony/Component/Intl/Resources/data/', @@ -65,10 +61,6 @@ ->notPath('#Symfony/Bridge/PhpUnit/.*Legacy#') // file content autogenerated by `var_export` ->notPath('Symfony/Component/Translation/Tests/Fixtures/resources.php') - // file content autogenerated by `VarExporter::export` - ->notPath('Symfony/Component/Serializer/Tests/Fixtures/serializer.class.metadata.php') - // test template - ->notPath('Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php') // explicit trigger_error tests ->notPath('Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php') // stop removing spaces on the end of the line in strings @@ -79,6 +71,10 @@ ->notPath('Symfony/Component/Cache/Traits/Redis6Proxy.php') ->notPath('Symfony/Component/Cache/Traits/RedisCluster5Proxy.php') ->notPath('Symfony/Component/Cache/Traits/RedisCluster6Proxy.php') + // svg + ->notPath('Symfony/Component/ErrorHandler/Resources/assets/images/symfony-ghost.svg.php') + // HTML templates + ->notPath('#Symfony/.*\.html\.php#') ) ->setCacheFile('.php-cs-fixer.cache') ; diff --git a/CHANGELOG-7.0.md b/CHANGELOG-7.0.md index beaf0489996bc..be632eae2db6f 100644 --- a/CHANGELOG-7.0.md +++ b/CHANGELOG-7.0.md @@ -7,6 +7,42 @@ in 7.0 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v7.0.0...v7.0.1 +* 7.0.9 (2024-06-28) + + * bug #57345 [DependencyInjection] Fix regression in ordering service locators by priority (longwave) + * bug #57553 [HttpKernel] Enable optional cache-warmers when cache-dir != build-dir (nicolas-grekas) + * bug #57497 [String] Fixed u()->snake(), b()->snake() and s()->snake() methods (arczinosek) + * bug #57574 [Filesystem] Fix Filesystem::remove() on Windows (nicolas-grekas) + * bug #57572 [DoctrineBridge] Fix compat with DI >= 6.4 (nicolas-grekas) + * bug #57538 [String] Add `alias` case to `EnglishInflector` (alexandre-daubois) + * bug #57533 [FrameworkBundle] Throw runtime exception when trying to use asset-mapper while http-client is disabled (nicolas-grekas) + * bug #57520 [SecurityBundle] Remove unused memory users’ `name` attribute from the XSD (MatTheCat) + * feature #57557 Ibexa is sponsoring Symfony 5.4, thanks to them! \o/ (nicolas-grekas) + * bug #57569 [HttpClient][Mailer] Revert "Let curl handle transfer encoding", use HTTP/1.1 for Mailgun (nicolas-grekas) + * bug #57499 [Mailer] Add additional headers in Scaleway bridge (MrMicky-FR) + * bug #57460 [VarExporter] fix contravariance problem with __unserialize() in lazy proxy (nikophil) + * bug #57397 [VarDumper] Fix FFI caster test (alexandre-daubois) + * bug #57453 [HttpClient] Fix parsing SSE (fancyweb) + * bug #57467 [SecurityBundle] Add `provider` XML attribute to the authenticators it’s missing from (MatTheCat) + * bug #57384 [Notifier] Fix thread key in GoogleChat bridge (romain-jacquart) + * bug #57372 [HttpKernel][Security] Fix accessing session for stateless request (VincentLanglet) + * bug #57112 [Messenger] Handle `AMQPConnectionException` when publishing a message (jwage) + * bug #57341 [Serializer] properly handle invalid data for false/true types (xabbuh) + * bug #57187 [Serializer] Fix `ObjectNormalizer` with property path (HypeMC) + * bug #57355 [ErrorHandler] Fix rendered exception code highlighting on PHP 8.3 (tscni) + * bug #57310 [DependencyInjection] Fix ternary in `AutowireCallable` attribute (alamirault) + * bug #57273 [FrameworkBundle] Fix setting default context for certain normalizers (HypeMC) + * bug #57395 [Notifier]  send the recipient phone number as an array (xabbuh) + * bug #52699 [Serializer] [PropertyAccessor] Ignore non-collection interface generics (mtarld) + * bug #54634 [String] Fix #54611 pluralization of -on ending words + singularization of -a ending foreign words (Geordie, DesLynx) + * bug #57213 [Validator] [UniqueValidator] Use correct variable as parameter in (custom) error message (seho-nl, Sebastien Hoek) + * bug #54920 [Messenger] Comply with Amazon SQS requirements for message body (VincentLanglet) + * bug #57321 [AssetMapper] fix npm version constraint conversion (Jean-Beru) + * bug #57110 [PhpUnitBridge] Fix error handler triggered outside of tests (HypeMC) + * bug #57297 [FrameworkBundle] not registered definitions must not be modified (xabbuh) + * bug #57234 [String] Fix Inflector for 'hardware' (podhy) + * bug #57224 [Mime] Use streams instead of loading raw message generator into memory (bytestream) + * 7.0.8 (2024-06-02) * bug #57284 [Mime] Fix TextPart using an unknown File (fabpot) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 2c65442650d09..92dac23ccbd1c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -13,8 +13,8 @@ The Symfony Connect username in parenthesis allows to get more information - Tobias Schultze (tobion) - Grégoire Pineau (lyrixx) - Thomas Calvet (fancyweb) - - Christophe Coevoet (stof) - Alexandre Daubois (alexandre-daubois) + - Christophe Coevoet (stof) - Wouter de Jong (wouterj) - Jordi Boggiano (seldaek) - Maxime Steinhausser (ogizanagi) @@ -35,9 +35,9 @@ The Symfony Connect username in parenthesis allows to get more information - Jérôme Tamarelle (gromnan) - Samuel ROZE (sroze) - Antoine Lamirault (alamirault) + - HypeMC (hypemc) - Pascal Borreli (pborreli) - Romain Neutron - - HypeMC (hypemc) - Joseph Bielawski (stloyd) - Drak (drak) - Abdellatif Ait boudad (aitboudad) @@ -61,6 +61,7 @@ The Symfony Connect username in parenthesis allows to get more information - William DURAND - ornicar - Dany Maillard (maidmaid) + - Simon André (simonandre) - Eriksen Costa - Diego Saint Esteben (dosten) - stealth35 ‏ (stealth35) @@ -69,20 +70,19 @@ The Symfony Connect username in parenthesis allows to get more information - Francis Besset (francisbesset) - Titouan Galopin (tgalopin) - Pierre du Plessis (pierredup) - - Simon André (simonandre) - David Maicher (dmaicher) - Bulat Shakirzyanov (avalanche123) - Iltar van der Berg - Miha Vrhovnik (mvrhov) + - Tomasz Kowalczyk (thunderer) - Gary PEGEOT (gary-p) + - Mathias Arlaud (mtarld) - Saša Stamenković (umpirsky) - Allison Guilhem (a_guilhem) - Mathieu Piot (mpiot) - Mathieu Santostefano (welcomattic) - Alexander Schranz (alexander-schranz) - Vasilij Duško (staff) - - Tomasz Kowalczyk (thunderer) - - Mathias Arlaud (mtarld) - Sarah Khalil (saro0h) - Laurent VOULLEMIER (lvo) - Konstantin Kudryashov (everzet) @@ -95,8 +95,8 @@ The Symfony Connect username in parenthesis allows to get more information - Dariusz Ruminski - Henrik Bjørnskov (henrikbjorn) - David Buchmann (dbu) - - Andrej Hudec (pulzarraider) - Ruud Kamphuis (ruudk) + - Andrej Hudec (pulzarraider) - Jáchym Toušek (enumag) - Christian Raue - Eric Clemmons (ericclemmons) @@ -162,9 +162,9 @@ The Symfony Connect username in parenthesis allows to get more information - Teoh Han Hui (teohhanhui) - Przemysław Bogusz (przemyslaw-bogusz) - Colin Frei + - Nicolas Philippe (nikophil) - excelwebzone - Paráda József (paradajozsef) - - Nicolas Philippe (nikophil) - Baptiste Clavié (talus) - Alexander Schwenn (xelaris) - Fabien Pennequin (fabienpennequin) @@ -181,24 +181,27 @@ The Symfony Connect username in parenthesis allows to get more information - François-Xavier de Guillebon (de-gui_f) - Maximilian Beckers (maxbeckers) - noniagriconomie + - Valtteri R (valtzu) - Eric GELOEN (gelo) - Gabriel Caruso - Stefano Sala (stefano.sala) - Ion Bazan (ionbazan) + - Niels Keurentjes (curry684) - OGAWA Katsuhiro (fivestar) - Jhonny Lidfors (jhonne) + - Dāvis Zālītis (k0d3r1s) - Juti Noppornpitak (shiroyuki) - Gregor Harlan (gharlan) + - Hugo Alliaume (kocal) - Anthony MARTIN - Andreas Schempp (aschempp) - Sebastian Hörl (blogsh) - Tigran Azatyan (tigranazatyan) + - Florent Mata (fmata) - Christopher Hertel (chertel) - Jonathan Scheiber (jmsche) - Daniel Gomes (danielcsgomes) - Hidenori Goto (hidenorigoto) - - Niels Keurentjes (curry684) - - Dāvis Zālītis (k0d3r1s) - Arnaud Kleinpeter (nanocom) - Guilherme Blanco (guilhermeblanco) - Saif Eddin Gmati (azjezz) @@ -207,10 +210,7 @@ The Symfony Connect username in parenthesis allows to get more information - SpacePossum - Richard van Laak (rvanlaak) - Andreas Braun - - Hugo Alliaume (kocal) - - Valtteri R (valtzu) - Pablo Godel (pgodel) - - Florent Mata (fmata) - Alessandro Chitolina (alekitto) - Rafael Dohms (rdohms) - Roman Martinuk (a2a4) @@ -220,6 +220,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jérôme Parmentier (lctrs) - Ahmed TAILOULOUTE (ahmedtai) - Simon Berger + - soyuka - Jérémy Derussé - Matthieu Napoli (mnapoli) - Tomas Votruba (tomas_votruba) @@ -240,6 +241,7 @@ 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 @@ -254,6 +256,7 @@ The Symfony Connect username in parenthesis allows to get more information - Matthieu Ouellette-Vachon (maoueh) - Michał Pipa (michal.pipa) - Dawid Nowak + - Philipp Wahala (hifi) - Jannik Zschiesche - Amal Raghav (kertz) - Jonathan Ingram @@ -270,7 +273,6 @@ The Symfony Connect username in parenthesis allows to get more information - Sébastien Alfaiate (seb33300) - James Halsall (jaitsu) - Christian Scheb - - Bob van de Vijver (bobvandevijver) - Guillaume (guill) - Mikael Pajunen - Warnar Boekkooi (boekkooi) @@ -298,7 +300,7 @@ The Symfony Connect username in parenthesis allows to get more information - Baptiste Leduc (korbeil) - Karoly Gossler (connorhu) - Timo Bakx (timobakx) - - soyuka + - Giorgio Premi - Ruben Gonzalez (rubenrua) - Benjamin Dulau (dbenjamin) - Markus Fasselt (digilist) @@ -317,9 +319,9 @@ The Symfony Connect username in parenthesis allows to get more information - sun (sun) - Larry Garfield (crell) - Leo Feyer - - Philipp Wahala (hifi) - Victor Bocharsky (bocharsky_bw) - Nikolay Labinskiy (e-moe) + - Asis Pattisahusiwa - Martin Schuhfuß (usefulthink) - apetitpa - Guilliam Xavier @@ -334,7 +336,7 @@ The Symfony Connect username in parenthesis allows to get more information - Nate Wiebe (natewiebe13) - Joe Bennett (kralos) - Leszek Prabucki (l3l0) - - Giorgio Premi + - Wojciech Kania - Thomas Lallement (raziel057) - Yassine Guedidi (yguedidi) - François Zaninotto (fzaninotto) @@ -399,16 +401,17 @@ The Symfony Connect username in parenthesis allows to get more information - Artem Lopata - Patrick McDougle (patrick-mcdougle) - Marc Weistroff (futurecat) + - Michał (bambucha15) - Danny Berger (dpb587) - Alif Rachmawadi - Anton Chernikov (anton_ch1989) - Pierre-Yves Lebecq (pylebecq) - Benjamin Leveque (benji07) - Jordan Samouh (jordansamouh) - - Wojciech Kania - Sullivan SENECHAL (soullivaneuh) - Loick Piera (pyrech) - Uwe Jäger (uwej711) + - javaDeveloperKid - W0rma - Lynn van der Berg (kjarli) - Michaël Perrin (michael.perrin) @@ -418,11 +421,11 @@ The Symfony Connect username in parenthesis allows to get more information - Marvin Petker - GordonsLondon - Ray - - Asis Pattisahusiwa - Philipp Cordes (corphi) - Chekote - Thomas Adam - Evert Harmeling (evertharmeling) + - Anderson Müller - jdhoek - Jurica Vlahoviček (vjurica) - Bob den Otter (bopp) @@ -467,9 +470,9 @@ The Symfony Connect username in parenthesis allows to get more information - Iker Ibarguren (ikerib) - Michael Holm (hollo) - Blanchon Vincent (blanchonvincent) - - Michał (bambucha15) - Christian Schmidt - Ben Hakim + - Stiven Llupa (sllupa) - Marco Petersen (ocrampete16) - Bohan Yang (brentybh) - Vilius Grigaliūnas @@ -478,7 +481,6 @@ The Symfony Connect username in parenthesis allows to get more information - Thomas Bisignani (toma) - Florian Klein (docteurklein) - Damien Alexandre (damienalexandre) - - javaDeveloperKid - Manuel Kießling (manuelkiessling) - Alexey Kopytko (sanmai) - Warxcell (warxcell) @@ -504,7 +506,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jan Decavele (jandc) - Gustavo Piltcher - Lee Rowlands - - Anderson Müller - Stepan Tanasiychuk (stfalcon) - Ivan Kurnosov - Tiago Ribeiro (fixe) @@ -540,6 +541,7 @@ The Symfony Connect username in parenthesis allows to get more information - Francesco Levorato - Vitaliy Zakharov (zakharovvi) - Tobias Sjösten (tobiassjosten) + - Michael Hirschler (mvhirsch) - Gyula Sallai (salla) - Hendrik Luup (hluup) - Inal DJAFAR (inalgnu) @@ -547,6 +549,7 @@ The Symfony Connect username in parenthesis allows to get more information - Martin Herndl (herndlm) - Dmytro Borysovskyi (dmytr0) - Johann Pardanaud + - Kai Dederichs - Pavel Kirpitsov (pavel-kirpichyov) - Robert Meijers - Artur Eshenbrener @@ -561,7 +564,6 @@ The Symfony Connect username in parenthesis allows to get more information - FORT Pierre-Louis (plfort) - Terje Bråten - Gonzalo Vilaseca (gonzalovilaseca) - - Stiven Llupa (sllupa) - Tarmo Leppänen (tarlepp) - Jakub Kucharovic (jkucharovic) - Daniel STANCU @@ -692,7 +694,6 @@ The Symfony Connect username in parenthesis allows to get more information - Desjardins Jérôme (jewome62) - Arturs Vonda - Matthew Smeets - - Michael Hirschler (mvhirsch) - Toni Rudolf (toooni) - Stefan Gehrig (sgehrig) - vagrant @@ -705,6 +706,7 @@ The Symfony Connect username in parenthesis allows to get more information - Restless-ET - Vlad Gregurco (vgregurco) - Artem Stepin (astepin) + - Jérémy DECOOL (jdecool) - Boris Vujicic (boris.vujicic) - Dries Vints - Judicaël RUFFIEUX (axanagor) @@ -722,7 +724,6 @@ The Symfony Connect username in parenthesis allows to get more information - Vitaliy Tverdokhlib (vitaliytv) - Ariel Ferrandini (aferrandini) - BASAK Semih (itsemih) - - Kai Dederichs - Dirk Pahl (dirkaholic) - Cédric Lombardot (cedriclombardot) - Jérémy REYNAUD (babeuloula) @@ -749,6 +750,7 @@ The Symfony Connect username in parenthesis allows to get more information - Roberto Espinoza (respinoza) - Pierre Rineau - Soufian EZ ZANTAR (soezz) + - Ivan Mezinov - Marek Zajac - Adam Harvey - ilyes kooli (skafandri) @@ -770,6 +772,7 @@ The Symfony Connect username in parenthesis allows to get more information - Andrey Astakhov (aast) - ReenExe - Fabian Lange (codingfabian) + - kylekatarnls (kylekatarnls) - Yoshio HANAWA - Jan van Thoor (janvt) - Joshua Nye @@ -1012,6 +1015,7 @@ The Symfony Connect username in parenthesis allows to get more information - Martins Sipenko - Guilherme Augusto Henschel - Rostyslav Kinash + - Christophe V. (cvergne) - Mardari Dorel (dorumd) - Daisuke Ohata - Vincent Simonin @@ -1021,6 +1025,7 @@ The Symfony Connect username in parenthesis allows to get more information - Andy Palmer (andyexeter) - Andrew Neil Forster (krciga22) - Stefan Warman (warmans) + - Faizan Akram Dar (faizanakram) - Tristan Maindron (tmaindron) - Behnoush Norouzali (behnoush) - Marko H. Tamminen (gzumba) @@ -1054,6 +1059,7 @@ The Symfony Connect username in parenthesis allows to get more information - Kevin SCHNEKENBURGER - Fabien Salles (blacked) - Andreas Erhard (andaris) + - alexpozzi - Michael Devery (mickadoo) - Gregor Nathanael Meyer (spackmat) - Antoine Corcy @@ -1171,15 +1177,16 @@ The Symfony Connect username in parenthesis allows to get more information - Alex Xandra Albert Sim - Sergey Yastrebov - Carson Full (carsonfull) - - kylekatarnls (kylekatarnls) - Steve Grunwell - Yuen-Chi Lian - Mathias Brodala (mbrodala) - Robert Fischer (sandoba) - Tarjei Huse (tarjei) + - mfettig - Besnik Br - Issam Raouf (iraouf) - Simon Mönch + - Sherin Bloemendaal - Jose Gonzalez - Jonathan (jlslew) - Claudio Zizza @@ -1188,6 +1195,7 @@ The Symfony Connect username in parenthesis allows to get more information - Christian Stoller (naitsirch) - Dave Marshall (davedevelopment) - Jakub Kulhan (jakubkulhan) + - Paweł Niedzielski (steveb) - Shaharia Azam - avorobiev - Gerben Oolbekkink @@ -1226,7 +1234,6 @@ The Symfony Connect username in parenthesis allows to get more information - Edvin Hultberg - shubhalgupta - Felds Liscia (felds) - - Jérémy DECOOL (jdecool) - Sergey Panteleev - Alexander Grimalovsky (flying) - Andrew Hilobok (hilobok) @@ -1276,6 +1283,7 @@ The Symfony Connect username in parenthesis allows to get more information - Cyril Pascal (paxal) - Pedro Casado (pdr33n) - Jayson Xu (superjavason) + - acoulton - DemigodCode - fago - Jan Prieser @@ -1453,6 +1461,7 @@ The Symfony Connect username in parenthesis allows to get more information - Robert Gruendler (pulse00) - Sebastian Paczkowski (sebpacz) - Simon Terrien (sterrien) + - Stephan Vierkant (svierkant) - Benoît Merlet (trompette) - Brad Jones - datibbaw @@ -1470,6 +1479,7 @@ The Symfony Connect username in parenthesis allows to get more information - Baptiste Leduc (bleduc) - soyuka - Patrick Kaufmann + - Ismail Özgün Turan (dadeather) - Mickael Perraud - Anton Dyshkant - Rafael Villa Verde @@ -1488,6 +1498,7 @@ The Symfony Connect username in parenthesis allows to get more information - Stewart Malik - Renan Taranto (renan-taranto) - Ninos Ego + - Samael tomas - Stefan Graupner (efrane) - Gemorroj (gemorroj) - Adrien Chinour @@ -1652,6 +1663,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ksaveras Šakys (xawiers) - Shaun Simmons - Ariel J. Birnbaum + - Yannick - Patrick Luca Fazzi (ap3ir0n) - Danijel Obradović - Pablo Borowicz @@ -1710,6 +1722,7 @@ The Symfony Connect username in parenthesis allows to get more information - Łukasz Chruściel (lchrusciel) - Jan Vernieuwe (vernija) - Antanas Arvasevicius + - Adam Kiss - Pierre Dudoret - Michal Trojanowski - Thomas @@ -1790,6 +1803,7 @@ The Symfony Connect username in parenthesis allows to get more information - Eddie Abou-Jaoude (eddiejaoude) - Haritz Iturbe (hizai) - Nerijus Arlauskas (nercury) + - Rutger Hertogh - Diego Sapriza - Joan Cruz - inspiran @@ -1854,6 +1868,7 @@ The Symfony Connect username in parenthesis allows to get more information - Thomason, James - Dario Savella - Gordienko Vladislav + - Joas Schilling - Ener-Getick - Moza Bogdan (bogdan_moza) - johan Vlaar @@ -1913,6 +1928,7 @@ The Symfony Connect username in parenthesis allows to get more information - Takashi Kanemoto (ttskch) - Aleksei Lebedev - dlorek + - Oriol Viñals - Stuart Fyfe - Jason Schilling (chapterjason) - David de Boer (ddeboer) @@ -1963,7 +1979,6 @@ The Symfony Connect username in parenthesis allows to get more information - Ismail Turan - error56 - Felicitus - - alexpozzi - Jorge Vahldick (jvahldick) - Krzysztof Przybyszewski (kprzybyszewski) - Vladimir Mantulo (mantulo) @@ -2057,7 +2072,6 @@ The Symfony Connect username in parenthesis allows to get more information - Adam Wójs (awojs) - Justin Reherman (jreherman) - Rubén Calvo (rubencm) - - Paweł Niedzielski (steveb) - Abdul.Mohsen B. A. A - Cédric Girard - Peter Jaap Blaakmeer @@ -2242,6 +2256,7 @@ The Symfony Connect username in parenthesis allows to get more information - Luis Galeas - Bogdan Scordaliu - Martin Pärtel + - PHAS Developer - Daniel Rotter (danrot) - Frédéric Bouchery (fbouchery) - Jacek Kobus (jackks) @@ -2260,7 +2275,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jeroen de Graaf - Ulrik McArdle - BiaDd - - mfettig - Oleksii Bulba - Ramon Cuñat - mboultoureau @@ -2317,6 +2331,7 @@ The Symfony Connect username in parenthesis allows to get more information - Starfox64 - Ivo Valchev - Thomas Hanke + - ffd000 - Daniel Tschinder - Thomas Durand - Arnaud CHASSEUX @@ -2330,7 +2345,6 @@ The Symfony Connect username in parenthesis allows to get more information - Rafał Muszyński (rafmus90) - Sébastien Decrême (sebdec) - Timothy Anido (xanido) - - acoulton - Mara Blaga - Rick Prent - skalpa @@ -2398,6 +2412,7 @@ The Symfony Connect username in parenthesis allows to get more information - Andrea Ruggiero (pupax) - Stan Jansen (stanjan) - Maxwell Vandervelde + - karstennilsen - kaywalker - Sebastian Ionescu - Robert Kopera @@ -2448,6 +2463,7 @@ The Symfony Connect username in parenthesis allows to get more information - tadas - Bastien Picharles - Kirk Madera + - Linas Ramanauskas - mamazu - Keith Maika - izenin @@ -2460,6 +2476,7 @@ The Symfony Connect username in parenthesis allows to get more information - Victor Garcia - Juan Mrad - Denis Yuzhanin + - k-sahara - Flavian Sierk - Rik van der Heijden - knezmilos13 @@ -2520,6 +2537,7 @@ The Symfony Connect username in parenthesis allows to get more information - tpetry - JustDylan23 - Juraj Surman + - ywisax - Martin Eckhardt - natechicago - Victor @@ -2572,6 +2590,7 @@ The Symfony Connect username in parenthesis allows to get more information - catch - aetxebeste - Roberto Guido + - ElisDN - roromix - Vitali Tsyrkin - Juga Paazmaya @@ -2953,6 +2972,7 @@ The Symfony Connect username in parenthesis allows to get more information - Patrizio Bekerle - Tom Maguire - Mateusz Lerczak + - Tim Porter - Richard Quadling - Rainrider - David Zuelke @@ -3063,6 +3083,7 @@ The Symfony Connect username in parenthesis allows to get more information - dakur - florian-michael-mast - tourze + - sam-bee - Vlad Dumitrache - wetternest - Erik van Wingerden @@ -3076,6 +3097,7 @@ The Symfony Connect username in parenthesis allows to get more information - Matheus Gontijo - Gerrit Drost - Linnaea Von Lavia + - Andrew Brown - Javan Eskander - Lenar Lõhmus - Cristian Gonzalez @@ -3307,6 +3329,7 @@ The Symfony Connect username in parenthesis allows to get more information - Karim Miladi - Michael Genereux - Greg Korba + - Camille Islasse - patrick-mcdougle - Tyler Stroud - Dariusz Czech @@ -3347,12 +3370,14 @@ The Symfony Connect username in parenthesis allows to get more information - wiseguy1394 - adam-mospan - Steve Hyde + - AbdelatifAitBara - nerdgod - Sam Williams - Ettore Del Negro - Guillaume Aveline - Adrian Philipp - James Michael DuPont + - Simone Ruggieri - Markus Tacker - Tomáš Votruba - Kasperki @@ -3420,6 +3445,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ayke Halder - Thorsten Hallwas - Brian Freytag + - Arend Hummeling - Marco Pfeiffer - Alex Nostadt - Michael Squires @@ -3502,6 +3528,7 @@ The Symfony Connect username in parenthesis allows to get more information - Yendric - ADmad - Nicolas Roudaire + - Marc Jauvin - Matthias Meyer - Abdouni Karim (abdounikarim) - Temuri Takalandze (abgeo) @@ -3544,7 +3571,6 @@ The Symfony Connect username in parenthesis allows to get more information - Elliot Anderson (elliot) - Erwan Nader (ernadoo) - Fabien D. (fabd) - - Faizan Akram Dar (faizanakram) - Carsten Eilers (fnc) - Sorin Gitlan (forapathy) - Fraller Balázs (fracsi) @@ -3626,7 +3652,6 @@ The Symfony Connect username in parenthesis allows to get more information - Christopher Georg (sky-chris) - Volker (skydiablo) - Julien Sanchez (sumbobyboys) - - Stephan Vierkant (svierkant) - Ron Gähler (t-ronx) - Guillermo Gisinger (t3chn0r) - Tom Newby (tomnewbyau) @@ -3637,6 +3662,7 @@ The Symfony Connect username in parenthesis allows to get more information - Moritz Kraft (userfriendly) - Víctor Mateo (victormateo) - Vincent MOULENE (vints24) + - Verlhac Gaëtan (viviengaetan) - David Grüner (vworldat) - Eugene Babushkin (warl) - Wouter Sioen (wouter_sioen) diff --git a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php index f7a2cac124d2b..302b1a23c77c9 100644 --- a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php +++ b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php @@ -58,6 +58,8 @@ function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) { } if (isset($this->fileMap[$name])) { $wrappedInstance = $this->load($this->fileMap[$name], false); + } elseif ((new \ReflectionMethod($this, $this->methodMap[$name]))->isStatic()) { + $wrappedInstance = $this->{$this->methodMap[$name]}($this, false); } else { $wrappedInstance = $this->{$this->methodMap[$name]}(false); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DummyManager.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DummyManager.php new file mode 100644 index 0000000000000..04e5a2acdd334 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DummyManager.php @@ -0,0 +1,69 @@ +register('foo', \stdClass::class)->setPublic(true); - $container->getDefinition('foo')->setLazy(true)->addTag('proxy', ['interface' => \stdClass::class]); + $container->register('foo', DummyManager::class)->setPublic(true); + $container->getDefinition('foo')->setLazy(true)->addTag('proxy', ['interface' => ObjectManager::class]); $container->compile(); $dumper = new PhpDumper($container); @@ -46,8 +48,8 @@ public function testResetService() $registry->resetManager(); $this->assertSame($foo, $container->get('foo')); - $this->assertInstanceOf(\stdClass::class, $foo); - $this->assertFalse(property_exists($foo, 'bar')); + $this->assertInstanceOf(ObjectManager::class, $foo); + $this->assertFalse(isset($foo->bar)); } /** @@ -79,7 +81,7 @@ public function testResetServiceWillNotNestFurtherLazyServicesWithinEachOther() $service = $container->get('foo'); - self::assertInstanceOf(\stdClass::class, $service); + self::assertInstanceOf(ObjectManager::class, $service); self::assertInstanceOf(LazyObjectInterface::class, $service); self::assertFalse($service->isLazyObjectInitialized()); @@ -92,7 +94,7 @@ public function testResetServiceWillNotNestFurtherLazyServicesWithinEachOther() $service->initializeLazyObject(); $wrappedValue = $service->initializeLazyObject(); - self::assertInstanceOf(\stdClass::class, $wrappedValue); + self::assertInstanceOf(DummyManager::class, $wrappedValue); self::assertNotInstanceOf(LazyObjectInterface::class, $wrappedValue); } @@ -104,10 +106,10 @@ private function dumpLazyServiceDoctrineBridgeContainerAsFiles() $container = new ContainerBuilder(); - $container->register('foo', \stdClass::class) + $container->register('foo', DummyManager::class) ->setPublic(true) ->setLazy(true) - ->addTag('proxy', ['interface' => \stdClass::class]); + ->addTag('proxy', ['interface' => ObjectManager::class]); $container->compile(); $fileSystem = new Filesystem(); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php index 4380bba494bba..fbfc2cb39b4ed 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php @@ -60,6 +60,13 @@ public function testAttributeWithGroupsAndPaylod() self::assertSame('some attached data', $constraint->payload); self::assertSame(['some_group'], $constraint->groups); } + + public function testValueOptionConfiguresFields() + { + $constraint = new UniqueEntity(['value' => 'email']); + + $this->assertSame('email', $constraint->fields); + } } #[UniqueEntity(['email'], message: 'myMessage')] diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index 7cf9111587835..8f000592c5b51 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\PhpUnit; +use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestResult; use PHPUnit\Runner\ErrorHandler; use PHPUnit\Util\Error\Handler; @@ -367,22 +368,26 @@ private static function getPhpUnitErrorHandler(): callable if ('PHPUnit\Util\ErrorHandler::handleError' === $eh) { return $eh; - } elseif (ErrorHandler::class === $eh) { - return function (int $errorNumber, string $errorString, string $errorFile, int $errorLine) { - ErrorHandler::instance()($errorNumber, $errorString, $errorFile, $errorLine); - - return true; - }; } foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { - if (isset($frame['object']) && $frame['object'] instanceof TestResult) { + if (!isset($frame['object'])) { + continue; + } + + if ($frame['object'] instanceof TestResult) { return new $eh( $frame['object']->getConvertDeprecationsToExceptions(), $frame['object']->getConvertErrorsToExceptions(), $frame['object']->getConvertNoticesToExceptions(), $frame['object']->getConvertWarningsToExceptions() ); + } elseif (ErrorHandler::class === $eh && $frame['object'] instanceof TestCase) { + return function (int $errorNumber, string $errorString, string $errorFile, int $errorLine) { + ErrorHandler::instance()($errorNumber, $errorString, $errorFile, $errorLine); + + return true; + }; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index de513066c7035..2b2fd4a0e739c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -339,7 +339,7 @@ public function load(array $configs, ContainerBuilder $container): void throw new LogicException('AssetMapper support cannot be enabled as the AssetMapper component is not installed. Try running "composer require symfony/asset-mapper".'); } - $this->registerAssetMapperConfiguration($config['asset_mapper'], $container, $loader, $this->readConfigEnabled('assets', $container, $config['assets'])); + $this->registerAssetMapperConfiguration($config['asset_mapper'], $container, $loader, $this->readConfigEnabled('assets', $container, $config['assets']), $this->readConfigEnabled('http_client', $container, $config['http_client'])); } else { $container->removeDefinition('cache.asset_mapper'); } @@ -1304,12 +1304,14 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co } } - private function registerAssetMapperConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $assetEnabled): void + private function registerAssetMapperConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $assetEnabled, bool $httpClientEnabled): void { $loader->load('asset_mapper.php'); - if (!$assetEnabled) { - $container->removeDefinition('asset_mapper.asset_package'); + if (!$httpClientEnabled) { + $container->register('asset_mapper.http_client', HttpClientInterface::class) + ->addTag('container.error') + ->addError('You cannot use the AssetMapper integration since the HttpClient component is not enabled. Try enabling the "framework.http_client" config option.'); } $paths = $config['paths']; @@ -1912,19 +1914,25 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->setParameter('serializer.default_context', $defaultContext); } + if (!$container->hasDefinition('serializer.normalizer.object')) { + return; + } + $arguments = $container->getDefinition('serializer.normalizer.object')->getArguments(); - $context = []; + $context = $arguments[6] ?? $defaultContext; if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) { - $context += ($arguments[6] ?? $defaultContext) + ['circular_reference_handler' => new Reference($config['circular_reference_handler'])]; + $context += ['circular_reference_handler' => new Reference($config['circular_reference_handler'])]; $container->getDefinition('serializer.normalizer.object')->setArgument(5, null); } if ($config['max_depth_handler'] ?? false) { - $context += ($arguments[6] ?? $defaultContext) + ['max_depth_handler' => new Reference($config['max_depth_handler'])]; + $context += ['max_depth_handler' => new Reference($config['max_depth_handler'])]; } $container->getDefinition('serializer.normalizer.object')->setArgument(6, $context); + + $container->getDefinition('serializer.normalizer.property')->setArgument(5, $defaultContext); } private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php index b7ce65f030345..404e7af18d0a1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php @@ -54,6 +54,8 @@ ]) ->alias(AssetMapperInterface::class, 'asset_mapper') + ->alias('asset_mapper.http_client', 'http_client') + ->set('asset_mapper.mapped_asset_factory', MappedAssetFactory::class) ->args([ service('asset_mapper.public_assets_path_resolver'), @@ -197,7 +199,7 @@ ]) ->set('asset_mapper.importmap.resolver', JsDelivrEsmResolver::class) - ->args([service('http_client')]) + ->args([service('asset_mapper.http_client')]) ->set('asset_mapper.importmap.renderer', ImportMapRenderer::class) ->args([ @@ -212,12 +214,12 @@ ->set('asset_mapper.importmap.auditor', ImportMapAuditor::class) ->args([ service('asset_mapper.importmap.config_reader'), - service('http_client'), + service('asset_mapper.http_client'), ]) ->set('asset_mapper.importmap.update_checker', ImportMapUpdateChecker::class) ->args([ service('asset_mapper.importmap.config_reader'), - service('http_client'), + service('asset_mapper.http_client'), ]) ->set('asset_mapper.importmap.command.require', ImportMapRequireCommand::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index 1135d37525340..c75776900d5b3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -140,7 +140,6 @@ service('property_info')->ignoreOnInvalid(), service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(), null, - [], ]) ->set('serializer.denormalizer.array', ArrayDenormalizer::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractWebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractWebTestCase.php index 085cb812eba69..17ff5ed732971 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractWebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractWebTestCase.php @@ -53,7 +53,7 @@ protected static function getKernelClass(): string protected static function createKernel(array $options = []): KernelInterface { - $class = self::getKernelClass(); + $class = static::getKernelClass(); if (!isset($options['test_case'])) { throw new \InvalidArgumentException('The option "test_case" must be set.'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php index 2856816d187a1..9d75c5bf675a2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php @@ -12,6 +12,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\TranslatableBackedEnum; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\AppKernel; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; /** * @author Kévin Dunglas @@ -35,39 +38,58 @@ public function testDeserializeArrayOfObject() $this->assertEquals($expected, $result); } - /** - * @dataProvider provideNormalizersAndEncodersWithDefaultContextOption - */ - public function testNormalizersAndEncodersUseDefaultContextConfigOption(string $normalizerId) + public function testNormalizersAndEncodersUseDefaultContextConfigOption() { - static::bootKernel(['test_case' => 'Serializer']); + /** @var SerializerKernel $kernel */ + $kernel = static::bootKernel(['test_case' => 'Serializer', 'root_config' => 'default_context.yaml']); + + foreach ($kernel->normalizersAndEncoders as $normalizerOrEncoderId) { + if (!static::getContainer()->has($normalizerOrEncoderId)) { + continue; + } + + $normalizerOrEncoder = static::getContainer()->get($normalizerOrEncoderId); - $normalizer = static::getContainer()->get($normalizerId); + $reflectionObject = new \ReflectionObject($normalizerOrEncoder); + $property = $reflectionObject->getProperty('defaultContext'); - $reflectionObject = new \ReflectionObject($normalizer); - $property = $reflectionObject->getProperty('defaultContext'); + $defaultContext = $property->getValue($normalizerOrEncoder); - $defaultContext = $property->getValue($normalizer); + self::assertArrayHasKey('fake_context_option', $defaultContext); + self::assertEquals('foo', $defaultContext['fake_context_option']); + } + } - self::assertArrayHasKey('fake_context_option', $defaultContext); - self::assertEquals('foo', $defaultContext['fake_context_option']); + protected static function getKernelClass(): string + { + return SerializerKernel::class; } +} + +class SerializerKernel extends AppKernel implements CompilerPassInterface +{ + public $normalizersAndEncoders = [ + 'serializer.normalizer.property.alias', // Special case as this normalizer isn't tagged + ]; - public static function provideNormalizersAndEncodersWithDefaultContextOption(): array + public function process(ContainerBuilder $container): void { - return [ - ['serializer.normalizer.constraint_violation_list.alias'], - ['serializer.normalizer.dateinterval.alias'], - ['serializer.normalizer.datetime.alias'], - ['serializer.normalizer.json_serializable.alias'], - ['serializer.normalizer.problem.alias'], - ['serializer.normalizer.uid.alias'], - ['serializer.normalizer.translatable.alias'], - ['serializer.normalizer.object.alias'], - ['serializer.encoder.xml.alias'], - ['serializer.encoder.yaml.alias'], - ['serializer.encoder.csv.alias'], - ]; + $services = array_merge( + $container->findTaggedServiceIds('serializer.normalizer'), + $container->findTaggedServiceIds('serializer.encoder') + ); + foreach ($services as $serviceId => $attributes) { + $class = $container->getDefinition($serviceId)->getClass(); + if (null === $reflectionConstructor = (new \ReflectionClass($class))->getConstructor()) { + continue; + } + foreach ($reflectionConstructor->getParameters() as $reflectionParam) { + if ('array $defaultContext' === $reflectionParam->getType()->getName().' $'.$reflectionParam->getName()) { + $this->normalizersAndEncoders[] = $serviceId.'.alias'; + break; + } + } + } } public function testSerializeTranslatableBackedEnum() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php index 06dc2d637a8e5..2fdbaea0fd9e8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php @@ -49,6 +49,11 @@ public function __construct($varDir, $testCase, $rootConfig, $environment, $debu parent::__construct($environment, $debug); } + protected function getContainerClass(): string + { + return parent::getContainerClass().substr(md5($this->rootConfig), -16); + } + public function registerBundles(): iterable { if (!file_exists($filename = $this->getProjectDir().'/'.$this->testCase.'/bundles.php')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml index 987cc384c7207..2f20dab9e8bc3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml @@ -10,7 +10,6 @@ framework: max_depth_handler: Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serializer\MaxDepthHandler default_context: enable_max_depth: true - fake_context_option: foo property_info: { enabled: true } services: @@ -18,54 +17,6 @@ services: alias: serializer public: true - serializer.normalizer.constraint_violation_list.alias: - alias: serializer.normalizer.constraint_violation_list - public: true - - serializer.normalizer.dateinterval.alias: - alias: serializer.normalizer.dateinterval - public: true - - serializer.normalizer.datetime.alias: - alias: serializer.normalizer.datetime - public: true - - serializer.normalizer.json_serializable.alias: - alias: serializer.normalizer.json_serializable - public: true - - serializer.normalizer.problem.alias: - alias: serializer.normalizer.problem - public: true - - serializer.normalizer.uid.alias: - alias: serializer.normalizer.uid - public: true - - serializer.normalizer.translatable.alias: - alias: serializer.normalizer.translatable - public: true - - serializer.normalizer.property.alias: - alias: serializer.normalizer.property - public: true - - serializer.normalizer.object.alias: - alias: serializer.normalizer.object - public: true - - serializer.encoder.xml.alias: - alias: serializer.encoder.xml - public: true - - serializer.encoder.yaml.alias: - alias: serializer.encoder.yaml - public: true - - serializer.encoder.csv.alias: - alias: serializer.encoder.csv - public: true - Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serializer\CircularReferenceHandler: ~ Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serializer\MaxDepthHandler: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/default_context.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/default_context.yaml new file mode 100644 index 0000000000000..de6114c5d4bb8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/default_context.yaml @@ -0,0 +1,59 @@ +imports: + - { resource: ../config/default.yml } + +framework: + serializer: + enabled: true + circular_reference_handler: ~ # This must be null + max_depth_handler: ~ # This must be null + default_context: + fake_context_option: foo + +services: + serializer.normalizer.constraint_violation_list.alias: + alias: serializer.normalizer.constraint_violation_list + public: true + + serializer.normalizer.dateinterval.alias: + alias: serializer.normalizer.dateinterval + public: true + + serializer.normalizer.datetime.alias: + alias: serializer.normalizer.datetime + public: true + + serializer.normalizer.json_serializable.alias: + alias: serializer.normalizer.json_serializable + public: true + + serializer.normalizer.object.alias: + alias: serializer.normalizer.object + public: true + + serializer.normalizer.problem.alias: + alias: serializer.normalizer.problem + public: true + + serializer.normalizer.property.alias: + alias: serializer.normalizer.property + public: true + + serializer.normalizer.uid.alias: + alias: serializer.normalizer.uid + public: true + + serializer.encoder.csv.alias: + alias: serializer.encoder.csv + public: true + + serializer.encoder.json.alias: + alias: serializer.encoder.json + public: true + + serializer.encoder.xml.alias: + alias: serializer.encoder.xml + public: true + + serializer.encoder.yaml.alias: + alias: serializer.encoder.yaml + public: true diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd index f5b69c7e5615c..a0806817a186c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd @@ -112,7 +112,6 @@ - @@ -206,6 +205,7 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php index d422675377afa..c5f04511752f1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php @@ -44,7 +44,7 @@ private function getForFirewall(): object if (!$this->locator->has($firewallName)) { $message = 'No '.$serviceIdentifier.' found for this firewall.'; if (\defined(static::class.'::FIREWALL_OPTION')) { - $message .= sprintf('Did you forget to add a "'.static::FIREWALL_OPTION.'" key under your "%s" firewall?', $firewallName); + $message .= sprintf(' Did you forget to add a "'.static::FIREWALL_OPTION.'" key under your "%s" firewall?', $firewallName); } throw new \LogicException($message); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml index 66dd30ea8d26a..f54c5064de23b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml @@ -64,9 +64,8 @@ - + - app.user_checker ROLE_USER diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml index 52a64d2f42908..e2f0e9865c251 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml @@ -15,7 +15,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml index a61d597fad573..e7f3e6873dfa8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml @@ -15,7 +15,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml index 1ba3c5e5098e4..462136c682cc5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml @@ -15,7 +15,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml index 314f25d263d71..cb82f2cc509f4 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml @@ -15,7 +15,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml index 6b51f236a50a7..2e0e75eabcb37 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml @@ -22,7 +22,6 @@ - diff --git a/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php b/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php index 7fb51772d6d3d..b8d9091d6cbee 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php @@ -126,10 +126,10 @@ public function fileExcerpt(string $file, int $line, int $srcContext = 3): ?stri if (\PHP_VERSION_ID >= 80300) { // remove main pre/code tags $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code); - // split multiline code tags - $code = preg_replace_callback('#]++)>((?:[^<]*+\\n)++[^<]*+)#', fn ($m) => "".str_replace("\n", "\n", $m[2]).'', $code); - // Convert spaces to html entities to preserve indentation when rendered - $code = str_replace(' ', ' ', $code); + // split multiline span tags + $code = preg_replace_callback('#]++)>((?:[^<\\n]*+\\n)++[^<]*+)#', function ($m) { + return "".str_replace("\n", "\n", $m[2]).''; + }, $code); $content = explode("\n", $code); } else { // remove main code/span tags diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php index e8ad69953cf1c..ebd2948c56790 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php @@ -27,7 +27,9 @@ */ class ImportMapRenderer { - private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL = 'https://ga.jspm.io/npm:es-module-shims@1.8.0/dist/es-module-shims.js'; + // https://generator.jspm.io/#S2NnYGAIzSvJLMlJTWEAAMYOgCAOAA + private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL = 'https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js'; + private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_INTEGRITY = 'sha384-ie1x72Xck445i0j4SlNJ5W5iGeL3Dpa0zD48MZopgWsjNB/lt60SuG1iduZGNnJn'; public function __construct( private readonly ImportMapGenerator $importMapGenerator, diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapVersionChecker.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapVersionChecker.php index b0af5736eb821..6a2cf579956bd 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapVersionChecker.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapVersionChecker.php @@ -137,7 +137,7 @@ public static function convertNpmConstraint(string $versionConstraint): ?string if (str_contains($segment, '-') && !preg_match('/-(alpha|beta|rc)\./', $segment)) { // This is a range [$start, $end] = explode('-', $segment); - $processedSegments[] = '>='.self::cleanVersionSegment(trim($start)).' <='.self::cleanVersionSegment(trim($end)); + $processedSegments[] = self::cleanVersionSegment(trim($start)).' - '.self::cleanVersionSegment(trim($end)); } elseif (preg_match('/^~(\d+\.\d+)$/', $segment, $matches)) { // Handle the tilde when only major.minor specified $baseVersion = $matches[1]; diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapVersionCheckerTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapVersionCheckerTest.php index 43346d3de33aa..2d6582c1d6cc4 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapVersionCheckerTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapVersionCheckerTest.php @@ -261,6 +261,26 @@ public static function getCheckVersionsTests() new PackageVersionProblem('foo', 'bar', 'some/repo', '1.5.0'), ], ]; + + yield 'single with range constraint but no problem' => [ + [ + self::createRemoteEntry('foo', version: '1.0'), + self::createRemoteEntry('bar', version: '2.0.3'), + ], + [ + 'foo' => ['bar'], + 'bar' => [], + ], + [ + [ + 'url' => '/foo/1.0', + 'response' => [ + 'dependencies' => ['bar' => '1.11 - 2'], + ], + ], + ], + [], + ]; } /** @@ -297,22 +317,22 @@ public static function getNpmSpecificVersionConstraints() // Hyphen Ranges yield 'hyphen range simple' => [ '1.0.0 - 2.0.0', - '>=1.0.0 <=2.0.0', + '1.0.0 - 2.0.0', ]; yield 'hyphen range with v prefix' => [ 'v1.0.0 - 2.0.0', - '>=1.0.0 <=2.0.0', + '1.0.0 - 2.0.0', ]; yield 'hyphen range without patch' => [ '1.0 - 2.0', - '>=1.0 <=2.0', + '1.0 - 2.0', ]; yield 'hyphen range with no spaces' => [ '1.0-v2.0', - '>=1.0 <=2.0', + '1.0 - 2.0', ]; // .x Wildcards @@ -386,7 +406,7 @@ public static function getNpmSpecificVersionConstraints() yield 'multiple constraints with space and or operator' => [ '1.2.7 || 1.2.9- v2.0.0', - '1.2.7 || >=1.2.9 <=2.0.0', + '1.2.7 || 1.2.9 - 2.0.0', ]; yield 'tilde constraint with patch version no change' => [ diff --git a/src/Symfony/Component/Console/Completion/CompletionInput.php b/src/Symfony/Component/Console/Completion/CompletionInput.php index 7ba41c0839da4..79c2f659a92c2 100644 --- a/src/Symfony/Component/Console/Completion/CompletionInput.php +++ b/src/Symfony/Component/Console/Completion/CompletionInput.php @@ -53,7 +53,7 @@ public static function fromString(string $inputStr, int $currentIndex): self * Create an input based on an COMP_WORDS token list. * * @param string[] $tokens the set of split tokens (e.g. COMP_WORDS or argv) - * @param $currentIndex the index of the cursor (e.g. COMP_CWORD) + * @param int $currentIndex the index of the cursor (e.g. COMP_CWORD) */ public static function fromTokens(array $tokens, int $currentIndex): self { diff --git a/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php b/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php index 5b6a8e42de94f..df0d081fd9acb 100644 --- a/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php +++ b/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php @@ -132,4 +132,19 @@ public static function provideFromStringData() yield ['bin/console cache:clear "multi word string"', ['bin/console', 'cache:clear', '"multi word string"']]; yield ['bin/console cache:clear \'multi word string\'', ['bin/console', 'cache:clear', '\'multi word string\'']]; } + + public function testToString() + { + $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 0); + $this->assertSame('foo| bar baz', (string) $input); + + $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 1); + $this->assertSame('foo bar| baz', (string) $input); + + $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 2); + $this->assertSame('foo bar baz|', (string) $input); + + $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 11); + $this->assertSame('foo bar baz |', (string) $input); + } } diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php b/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php index f14d9066d33d0..da9ac0d0014d9 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php @@ -42,7 +42,7 @@ public function __construct( public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition { - return (new Definition($type = \is_string($this->lazy) ? $this->lazy : ($type ?: 'Closure'))) + return (new Definition($type = \is_array($this->lazy) ? current($this->lazy) : ($type ?: 'Closure'))) ->setFactory(['Closure', 'fromCallable']) ->setArguments([\is_array($value) ? $value + [1 => '__invoke'] : $value]) ->setLazy($this->lazy || 'Closure' !== $type && 'callable' !== (string) $parameter->getType()); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php index 728feb0bd732f..032e905095c5d 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php @@ -131,7 +131,6 @@ public static function map(array $services): array $services[$k] = new ServiceClosureArgument($v); } - ksort($services); return $services; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireCallableTest.php b/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireCallableTest.php index f5aeb35d44939..9e1a0d85429ff 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireCallableTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireCallableTest.php @@ -93,4 +93,35 @@ public function testArrayCallableWithServiceOnly() self::assertEquals([new Reference('my_service'), '__invoke'], $attribute->value); self::assertFalse($attribute->lazy); } + + public function testLazyAsArrayInDefinition() + { + $attribute = new AutowireCallable(callable: [Foo::class, 'myMethod'], lazy: 'my_lazy_class'); + + self::assertSame([Foo::class, 'myMethod'], $attribute->value); + + $definition = $attribute->buildDefinition('my_value', 'my_custom_type', new \ReflectionParameter([Foo::class, 'myMethod'], 'myParameter')); + + self::assertSame('my_lazy_class', $definition->getClass()); + self::assertTrue($definition->isLazy()); + } + + public function testLazyIsFalseInDefinition() + { + $attribute = new AutowireCallable(callable: [Foo::class, 'myMethod'], lazy: false); + + self::assertFalse($attribute->lazy); + + $definition = $attribute->buildDefinition('my_value', 'my_custom_type', new \ReflectionParameter([Foo::class, 'myMethod'], 'myParameter')); + + self::assertSame('my_custom_type', $definition->getClass()); + self::assertFalse($definition->isLazy()); + } +} + +class Foo +{ + public function myMethod(callable $myParameter) + { + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php index cd0ac69738674..0916e21314392 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php @@ -623,7 +623,7 @@ public function testTaggedLocatorWithDefaultPriorityMethodConfiguredViaAttribute // We need to check priority of instances in the factories $factories = (new \ReflectionClass($locator))->getProperty('factories'); - self::assertSame([BarTagClass::class, FooTagClass::class], array_keys($factories->getValue($locator))); + self::assertSame([FooTagClass::class, BarTagClass::class], array_keys($factories->getValue($locator))); } public function testTaggedLocatorWithDefaultIndexMethodAndWithDefaultPriorityMethodConfiguredViaAttribute() @@ -652,7 +652,7 @@ public function testTaggedLocatorWithDefaultIndexMethodAndWithDefaultPriorityMet // We need to check priority of instances in the factories $factories = (new \ReflectionClass($locator))->getProperty('factories'); - self::assertSame(['bar_tag_class', 'foo_tag_class'], array_keys($factories->getValue($locator))); + self::assertSame(['foo_tag_class', 'bar_tag_class'], array_keys($factories->getValue($locator))); self::assertSame($container->get(BarTagClass::class), $locator->get('bar_tag_class')); self::assertSame($container->get(FooTagClass::class), $locator->get('foo_tag_class')); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index b5e2458c337e3..d9e3e921eab5c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -462,7 +462,7 @@ public static function getSubscribedServices(): array 'autowired' => new ServiceClosureArgument(new TypedReference('service.id', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'autowired', [new Autowire(service: 'service.id')])), 'autowired.nullable' => new ServiceClosureArgument(new TypedReference('service.id', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'autowired.nullable', [new Autowire(service: 'service.id')])), 'autowired.parameter' => new ServiceClosureArgument('foobar'), - 'autowire.decorated' => new ServiceClosureArgument(new Reference('.service_locator.oO4rxCy.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)), + 'autowire.decorated' => new ServiceClosureArgument(new Reference('.service_locator.0tSxobl.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)), 'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'target', [new Target('someTarget')])), ]; $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php index faeb743162d11..812b47c7a6f1f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php @@ -214,7 +214,7 @@ public function testDefinitionOrderIsTheSame() $locator = $container->getDefinition($locator); $factories = $locator->getArguments()[0]; - static::assertSame(['service-1', 'service-2'], array_keys($factories)); + static::assertSame(['service-2', 'service-1'], array_keys($factories)); } public function testBindingsAreProcessed() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php index 0565bd68ce279..b3368a0990037 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php @@ -43,9 +43,9 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.service_locator.2hyyc9y' => true, - '.service_locator.KGUGnmw' => true, - '.service_locator.KGUGnmw.foo_service' => true, + '.service_locator.0H1ht0q' => true, + '.service_locator.0H1ht0q.foo_service' => true, + '.service_locator.tfSHZa1' => true, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true, ]; } @@ -68,14 +68,14 @@ protected static function getTestServiceSubscriberService($container) protected static function getFooServiceService($container) { return $container->services['foo_service'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber((new \Symfony\Component\DependencyInjection\Argument\ServiceLocator($container->getService ??= $container->getService(...), [ - 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => ['privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'getCustomDefinitionService', false], 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => ['services', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', 'getTestServiceSubscriberService', false], + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => ['privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'getCustomDefinitionService', false], 'bar' => ['services', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', 'getTestServiceSubscriberService', false], 'baz' => ['privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'getCustomDefinitionService', false], 'late_alias' => ['services', 'late_alias', 'getLateAliasService', false], ], [ - 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'bar' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'baz' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'late_alias' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestDefinition1', diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php index b6d76ff151516..17745c8170818 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php @@ -252,10 +252,10 @@ private function fileExcerpt(string $file, int $line, int $srcContext = 3): stri if (\PHP_VERSION_ID >= 80300) { // remove main pre/code tags $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code); - // split multiline code tags - $code = preg_replace_callback('#]++)>((?:[^<]*+\\n)++[^<]*+)#', fn ($m) => "".str_replace("\n", "\n", $m[2]).'', $code); - // Convert spaces to html entities to preserve indentation when rendered - $code = str_replace(' ', ' ', $code); + // split multiline span tags + $code = preg_replace_callback('#]++)>((?:[^<\\n]*+\\n)++[^<]*+)#', function ($m) { + return "".str_replace("\n", "\n", $m[2]).''; + }, $code); $content = explode("\n", $code); } else { // remove main code/span tags diff --git a/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css b/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css index 3e6eae5a92273..e4d1f11e928ea 100644 --- a/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css +++ b/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css @@ -349,7 +349,7 @@ header .container { display: flex; justify-content: space-between; } .trace-code li { color: #969896; margin: 0; padding-left: 10px; float: left; width: 100%; } .trace-code li + li { margin-top: 5px; } .trace-code li.selected { background: var(--trace-selected-background); margin-top: 2px; } -.trace-code li code { color: var(--base-6); white-space: nowrap; } +.trace-code li code { color: var(--base-6); white-space: pre; } .trace-as-text .stacktrace { line-height: 1.8; margin: 0 0 15px; white-space: pre-wrap; } diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 648c8d1db9044..7ab25d2fd5202 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -164,7 +164,7 @@ private static function doRemove(array $files, bool $isRecursive): void } } elseif (is_dir($file)) { if (!$isRecursive) { - $tmpName = \dirname(realpath($file)).'/.'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-_')); + $tmpName = \dirname(realpath($file)).'/.!'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-!')); if (file_exists($tmpName)) { try { diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index bc8a5cecaa259..4e1d9351a69ba 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -171,17 +171,17 @@ public function testCopyForOriginUrlsAndExistingLocalFileDefaultsToCopy() } $finder = new PhpExecutableFinder(); - $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', 'localhost:8057'])); + $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', 'localhost:8857'])); $process->setWorkingDirectory(__DIR__.'/Fixtures/web'); $process->start(); do { usleep(50000); - } while (!@fopen('http://localhost:8057', 'r')); + } while (!@fopen('http://localhost:8857', 'r')); try { - $sourceFilePath = 'http://localhost:8057/logo_symfony_header.png'; + $sourceFilePath = 'http://localhost:8857/logo_symfony_header.png'; $targetFilePath = $this->workspace.\DIRECTORY_SEPARATOR.'copy_target_file'; file_put_contents($targetFilePath, 'TARGET FILE'); $this->filesystem->copy($sourceFilePath, $targetFilePath, false); diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index fef0ad09a9079..4446a031e9695 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -250,8 +250,9 @@ public function request(string $method, string $url, array $options = []): Respo if (isset($options['normalized_headers']['content-length'][0])) { $curlopts[\CURLOPT_INFILESIZE] = (int) substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: ')); - } elseif (!isset($options['normalized_headers']['transfer-encoding'])) { - $curlopts[\CURLOPT_INFILESIZE] = -1; + } + if (!isset($options['normalized_headers']['transfer-encoding'])) { + $curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding:'.(isset($curlopts[\CURLOPT_INFILESIZE]) ? '' : ' chunked'); } if ('POST' !== $method) { diff --git a/src/Symfony/Component/HttpClient/EventSourceHttpClient.php b/src/Symfony/Component/HttpClient/EventSourceHttpClient.php index 4e551ac0409f6..b5f88ddbabe8f 100644 --- a/src/Symfony/Component/HttpClient/EventSourceHttpClient.php +++ b/src/Symfony/Component/HttpClient/EventSourceHttpClient.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpClient; +use Symfony\Component\HttpClient\Chunk\DataChunk; use Symfony\Component\HttpClient\Chunk\ServerSentEvent; use Symfony\Component\HttpClient\Exception\EventSourceException; use Symfony\Component\HttpClient\Response\AsyncContext; @@ -121,17 +122,30 @@ public function request(string $method, string $url, array $options = []): Respo return; } - $rx = '/((?:\r\n){2,}|\r{2,}|\n{2,})/'; - $content = $state->buffer.$chunk->getContent(); - if ($chunk->isLast()) { - $rx = substr_replace($rx, '|$', -2, 0); + if ('' !== $content = $state->buffer) { + $state->buffer = ''; + yield new DataChunk(-1, $content); + } + + yield $chunk; + + return; } - $events = preg_split($rx, $content, -1, \PREG_SPLIT_DELIM_CAPTURE); + + $content = $state->buffer.$chunk->getContent(); + $events = preg_split('/((?:\r\n){2,}|\r{2,}|\n{2,})/', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); $state->buffer = array_pop($events); for ($i = 0; isset($events[$i]); $i += 2) { - $event = new ServerSentEvent($events[$i].$events[1 + $i]); + $content = $events[$i].$events[1 + $i]; + if (!preg_match('/(?:^|\r\n|[\r\n])[^:\r\n]/', $content)) { + yield new DataChunk(-1, $content); + + continue; + } + + $event = new ServerSentEvent($content); if ('' !== $event->getId()) { $context->setInfo('last_event_id', $state->lastEventId = $event->getId()); @@ -143,17 +157,6 @@ public function request(string $method, string $url, array $options = []): Respo yield $event; } - - if (preg_match('/^(?::[^\r\n]*+(?:\r\n|[\r\n]))+$/m', $state->buffer)) { - $content = $state->buffer; - $state->buffer = ''; - - yield $context->createChunk($content); - } - - if ($chunk->isLast()) { - yield $chunk; - } }); } } diff --git a/src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php index 629655392ce81..de199ac729a59 100644 --- a/src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php @@ -15,9 +15,11 @@ use Symfony\Component\HttpClient\Chunk\DataChunk; use Symfony\Component\HttpClient\Chunk\ErrorChunk; use Symfony\Component\HttpClient\Chunk\FirstChunk; +use Symfony\Component\HttpClient\Chunk\LastChunk; use Symfony\Component\HttpClient\Chunk\ServerSentEvent; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Component\HttpClient\Exception\EventSourceException; +use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpClient\Response\ResponseStream; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -34,7 +36,11 @@ class EventSourceHttpClientTest extends TestCase */ public function testGetServerSentEvents(string $sep) { - $rawData = <<assertSame(['Accept: text/event-stream', 'Cache-Control: no-cache'], $options['headers']); + + return new MockResponse([ + str_replace("\n", $sep, << false, 'http_method' => 'GET', 'url' => 'http://localhost:8080/events', 'response_headers' => ['content-type: text/event-stream']]); - $responseStream = new ResponseStream((function () use ($response, $chunk) { - yield $response => new FirstChunk(); - yield $response => $chunk; - yield $response => new ErrorChunk(0, 'timeout'); - })()); - - $hasCorrectHeaders = function ($options) { - $this->assertSame(['Accept: text/event-stream', 'Cache-Control: no-cache'], $options['headers']); - - return true; - }; - - $httpClient = $this->createMock(HttpClientInterface::class); - $httpClient->method('request')->with('GET', 'http://localhost:8080/events', $this->callback($hasCorrectHeaders))->willReturn($response); - - $httpClient->method('stream')->willReturn($responseStream); - - $es = new EventSourceHttpClient($httpClient); +TXT + ), + ], [ + 'canceled' => false, + 'http_method' => 'GET', + 'url' => 'http://localhost:8080/events', + 'response_headers' => ['content-type: text/event-stream'], + ]); + })); $res = $es->connect('http://localhost:8080/events'); $expected = [ new FirstChunk(), new ServerSentEvent(str_replace("\n", $sep, "event: builderror\nid: 46\ndata: {\"foo\": \"bar\"}\n\n")), new ServerSentEvent(str_replace("\n", $sep, "event: reload\nid: 47\ndata: {}\n\n")), - new ServerSentEvent(str_replace("\n", $sep, "event: reload\nid: 48\ndata: {}\n\n")), + new DataChunk(-1, str_replace("\n", $sep, ": this is a oneline comment\n\n")), + new DataChunk(-1, str_replace("\n", $sep, ": this is a\n: multiline comment\n\n")), + new ServerSentEvent(str_replace("\n", $sep, ": comments are ignored\nevent: reload\n: anywhere\nid: 48\ndata: {}\n\n")), new ServerSentEvent(str_replace("\n", $sep, "data: test\ndata:test\nid: 49\nevent: testEvent\n\n\n")), new ServerSentEvent(str_replace("\n", $sep, "id: 50\ndata: \ndata\ndata: \ndata\ndata: \n\n")), + new DataChunk(-1, str_replace("\n", $sep, "id: 60\ndata")), + new LastChunk("\r\n" === $sep ? 355 : 322), ]; - $i = 0; - - $this->expectExceptionMessage('Response has been canceled'); - while ($res) { - if ($i > 0) { - $res->cancel(); - } - foreach ($es->stream($res) as $chunk) { - if ($chunk->isTimeout()) { - continue; - } - - if ($chunk->isLast()) { - continue; - } - - $this->assertEquals($expected[$i++], $chunk); - } + foreach ($es->stream($res) as $chunk) { + $this->assertEquals(array_shift($expected), $chunk); } + $this->assertSame([], $expected); } public function testPostServerSentEvents() diff --git a/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php index 1b9c04476661a..07a298f57be0e 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php @@ -63,7 +63,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep $sessionMetadata = []; $sessionAttributes = []; $flashes = []; - if ($request->hasSession()) { + if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()) { $session = $request->getSession(); if ($session->isStarted()) { $sessionMetadata['Created'] = date(\DATE_RFC822, $session->getMetadataBag()->getCreated()); diff --git a/src/Symfony/Component/HttpKernel/Event/KernelEvent.php b/src/Symfony/Component/HttpKernel/Event/KernelEvent.php index e64cc419b91e4..02426c52a19d7 100644 --- a/src/Symfony/Component/HttpKernel/Event/KernelEvent.php +++ b/src/Symfony/Component/HttpKernel/Event/KernelEvent.php @@ -16,7 +16,7 @@ use Symfony\Contracts\EventDispatcher\Event; /** - * Base class for events thrown in the HttpKernel component. + * Base class for events dispatched in the HttpKernel component. * * @author Bernhard Schussek */ diff --git a/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php b/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php index 5d16823fceddd..1f30582f4a010 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php @@ -97,7 +97,7 @@ public function onKernelResponse(ResponseEvent $event): void return; } - $session = $request->hasPreviousSession() ? $request->getSession() : null; + $session = !$request->attributes->getBoolean('_stateless') && $request->hasPreviousSession() ? $request->getSession() : null; if ($session instanceof Session) { $usageIndexValue = $usageIndexReference = &$session->getUsageIndex(); diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 37e75219fd7f8..34e48d17ced7f 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 = '7.0.8'; - public const VERSION_ID = 70008; + public const VERSION = '7.0.9'; + public const VERSION_ID = 70009; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 0; - public const RELEASE_VERSION = 8; + public const RELEASE_VERSION = 9; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2024'; @@ -521,10 +521,17 @@ protected function initializeContainer(): void touch($oldContainerDir.'.legacy'); } - $preload = $this instanceof WarmableInterface ? (array) $this->warmUp($this->container->getParameter('kernel.cache_dir'), $buildDir) : []; + $cacheDir = $this->container->getParameter('kernel.cache_dir'); + $preload = $this instanceof WarmableInterface ? (array) $this->warmUp($cacheDir, $buildDir) : []; if ($this->container->has('cache_warmer')) { - $preload = array_merge($preload, (array) $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir'), $buildDir)); + $cacheWarmer = $this->container->get('cache_warmer'); + + if ($cacheDir !== $buildDir) { + $cacheWarmer->enableOptionalWarmers(); + } + + $preload = array_merge($preload, (array) $cacheWarmer->warmUp($cacheDir, $buildDir)); } if ($preload && file_exists($preloadFile = $buildDir.'/'.$class.'.preload.php')) { diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php index 57f8f53b1e9f7..fdf550d0ecd41 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php @@ -40,8 +40,8 @@ public function testKernelTerminate() ->willReturn($profile); $kernel = $this->createMock(HttpKernelInterface::class); - $mainRequest = $this->createMock(Request::class); - $subRequest = $this->createMock(Request::class); + $mainRequest = new Request(); + $subRequest = new Request(); $response = $this->createMock(Response::class); $requestStack = new RequestStack(); diff --git a/src/Symfony/Component/Mailer/Bridge/Infobip/README.md b/src/Symfony/Component/Mailer/Bridge/Infobip/README.md index c86458c3991f9..b040f259e20b5 100644 --- a/src/Symfony/Component/Mailer/Bridge/Infobip/README.md +++ b/src/Symfony/Component/Mailer/Bridge/Infobip/README.md @@ -12,10 +12,24 @@ MAILER_DSN=infobip+api://KEY@BASE_URL MAILER_DSN=infobip+smtp://KEY@default ``` +Custom Headers +-------------- + +This transport supports the following custom headers: + +| Header | Type | Description | +| -------------------------------- | ------- | -------------------------------------------------------------------------------------------- | +| `X-Infobip-IntermediateReport` | boolean | The real-time Intermediate delivery report that will be sent on your callback server. | +| `X-Infobip-NotifyUrl` | string | The URL on your callback server on which the Delivery report will be sent. | +| `X-Infobip-NotifyContentType` | string | Preferred Delivery report content type. Can be application/json or application/xml. | +| `X-Infobip-MessageId` | string | The ID that uniquely identifies the message sent to a recipient. | +| `X-Infobip-Track` | boolean | Enable or disable open and click tracking. | + Resources --------- -* [Infobip Api Docs](https://www.infobip.com/docs/api#channels/email) -* [Contributing](https://symfony.com/doc/current/contributing/index.html) -* [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) + + * [Infobip Api Docs](https://www.infobip.com/docs/api#channels/email) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHttpTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHttpTransport.php index 626a494d158fc..ca0100918dde8 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHttpTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHttpTransport.php @@ -63,6 +63,7 @@ protected function doSendHttp(SentMessage $message): ResponseInterface $endpoint = sprintf('%s/v3/%s/messages.mime', $this->getEndpoint(), urlencode($this->domain)); $response = $this->client->request('POST', 'https://'.$endpoint, [ + 'http_version' => '1.1', 'auth_basic' => 'api:'.$this->key, 'headers' => $headers, 'body' => $body->bodyToIterable(), diff --git a/src/Symfony/Component/Mailer/Bridge/Scaleway/Tests/Transport/ScalewayApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Scaleway/Tests/Transport/ScalewayApiTransportTest.php index 0d91d002cd21c..f31e041ea73d0 100644 --- a/src/Symfony/Component/Mailer/Bridge/Scaleway/Tests/Transport/ScalewayApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Scaleway/Tests/Transport/ScalewayApiTransportTest.php @@ -69,6 +69,8 @@ public function testSend() $this->assertSame('attachment.txt', $body['attachments'][0]['name']); $this->assertSame('text/plain', $body['attachments'][0]['type']); $this->assertSame(base64_encode('some attachment'), $body['attachments'][0]['content']); + $this->assertSame('Reply-To', $body['additional_headers'][0]['key']); + $this->assertStringContainsString('foo@bar.fr', $body['additional_headers'][0]['value']); return new JsonMockResponse(['emails' => [['message_id' => 'foobar']]], [ 'http_code' => 200, @@ -81,6 +83,7 @@ public function testSend() $mail->subject('Hello!') ->to(new Address('saif.gmati@symfony.com', 'Saif Eddin')) ->from(new Address('fabpot@symfony.com', 'Fabien')) + ->replyTo(new Address('foo@bar.fr', 'Foo')) ->text('Hello There!') ->addPart(new DataPart('some attachment', 'attachment.txt', 'text/plain')); diff --git a/src/Symfony/Component/Mailer/Bridge/Scaleway/Transport/ScalewayApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Scaleway/Transport/ScalewayApiTransport.php index 60cb198d3b5ab..3c277abb6e719 100644 --- a/src/Symfony/Component/Mailer/Bridge/Scaleway/Transport/ScalewayApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Scaleway/Transport/ScalewayApiTransport.php @@ -101,6 +101,9 @@ private function getPayload(Email $email, Envelope $envelope): array if ($attachements = $this->prepareAttachments($email)) { $payload['attachments'] = $attachements; } + if ($headers = $this->getCustomHeaders($email)) { + $payload['additional_headers'] = $headers; + } return $payload; } @@ -122,6 +125,24 @@ private function prepareAttachments(Email $email): array return $attachments; } + private function getCustomHeaders(Email $email): array + { + $headers = []; + $headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender']; + foreach ($email->getHeaders()->all() as $name => $header) { + if (\in_array($name, $headersToBypass, true)) { + continue; + } + + $headers[] = [ + 'key' => $header->getName(), + 'value' => $header->getBodyAsString(), + ]; + } + + return $headers; + } + private function formatAddress(Address $address): array { $array = ['email' => $address->getAddress()]; diff --git a/src/Symfony/Component/Mailer/MailerInterface.php b/src/Symfony/Component/Mailer/MailerInterface.php index 8d9540a3e5e3f..ebac4b53efa4e 100644 --- a/src/Symfony/Component/Mailer/MailerInterface.php +++ b/src/Symfony/Component/Mailer/MailerInterface.php @@ -15,7 +15,7 @@ use Symfony\Component\Mime\RawMessage; /** - * Interface for mailers able to send emails synchronous and/or asynchronous. + * Interface for mailers able to send emails synchronously and/or asynchronously. * * Implementations must support synchronous and asynchronous sending. * diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsSenderTest.php b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsSenderTest.php index 80840c859cb05..d11a5d8037b27 100644 --- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsSenderTest.php +++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsSenderTest.php @@ -72,4 +72,19 @@ public function testSendWithAmazonSqsXrayTraceHeaderStamp() $sender = new AmazonSqsSender($connection, $serializer); $sender->send($envelope); } + + public function testSendEncodeBodyToRespectAmazonRequirements() + { + $envelope = new Envelope(new DummyMessage('Oy')); + $encoded = ['body' => "\x7", 'headers' => ['type' => DummyMessage::class]]; + + $connection = $this->createMock(Connection::class); + $connection->expects($this->once())->method('send')->with(base64_encode($encoded['body']), $encoded['headers']); + + $serializer = $this->createMock(SerializerInterface::class); + $serializer->method('encode')->with($envelope)->willReturn($encoded); + + $sender = new AmazonSqsSender($connection, $serializer); + $sender->send($envelope); + } } diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsSender.php b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsSender.php index ca13e0c737311..eba67c1ca1b2c 100644 --- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsSender.php +++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsSender.php @@ -35,6 +35,7 @@ public function __construct(Connection $connection, SerializerInterface $seriali public function send(Envelope $envelope): Envelope { $encodedMessage = $this->serializer->encode($envelope); + $encodedMessage = $this->complyWithAmazonSqsRequirements($encodedMessage); /** @var DelayStamp|null $delayStamp */ $delayStamp = $envelope->last(DelayStamp::class); @@ -69,4 +70,20 @@ public function send(Envelope $envelope): Envelope return $envelope; } + + /** + * @see https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html + * + * @param array{body: string, headers?: array} $encodedMessage + * + * @return array{body: string, headers?: array} + */ + private function complyWithAmazonSqsRequirements(array $encodedMessage): array + { + if (preg_match('/[^\x20-\x{D7FF}\xA\xD\x9\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', $encodedMessage['body'])) { + $encodedMessage['body'] = base64_encode($encodedMessage['body']); + } + + return $encodedMessage; + } } diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrinePostgreSqlFilterIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrinePostgreSqlFilterIntegrationTest.php new file mode 100644 index 0000000000000..9a4738be2ed97 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrinePostgreSqlFilterIntegrationTest.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport; + +use Doctrine\DBAL\Configuration; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use Doctrine\DBAL\Schema\Sequence; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Tools\DsnParser; +use Doctrine\DBAL\Types\Type; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\PostgreSqlConnection; + +/** + * This test checks on a postgres connection whether the doctrine asset filter works as expected. + * + * @requires extension pdo_pgsql + * + * @group integration + */ +class DoctrinePostgreSqlFilterIntegrationTest extends TestCase +{ + private Connection $driverConnection; + + protected function setUp(): void + { + if (!$host = getenv('POSTGRES_HOST')) { + $this->markTestSkipped('Missing POSTGRES_HOST env variable'); + } + + $url = "pdo-pgsql://postgres:password@$host"; + $params = (new DsnParser())->parse($url); + $config = new Configuration(); + if (class_exists(DefaultSchemaManagerFactory::class)) { + $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + } + + $this->driverConnection = DriverManager::getConnection($params, $config); + + $this->createAssets(); + } + + protected function tearDown(): void + { + $this->removeAssets(); + + $this->driverConnection->close(); + } + + public function testFilterAssets() + { + $schemaManager = $this->driverConnection->createSchemaManager(); + + $this->assertFalse($schemaManager->tablesExist(['queue_table'])); + $this->assertTrue($schemaManager->tablesExist(['app_table'])); + $this->assertTrue($this->hasSequence('app_table_id')); + + $connection = new PostgreSqlConnection(['table_name' => 'queue_table'], $this->driverConnection); + $connection->setup(); + + $schemaManager = $this->driverConnection->createSchemaManager(); + + $this->assertTrue($schemaManager->tablesExist(['queue_table'])); + $this->assertTrue($schemaManager->tablesExist(['app_table'])); + $this->assertTrue($this->hasSequence('app_table_id')); + } + + private function createAssets(): void + { + $this->removeAssets(); + + $schemaManager = $this->driverConnection->createSchemaManager(); + $schemaManager->createTable(new Table('app_table', [new Column('id', Type::getType('integer'))])); + $schemaManager->createSequence(new Sequence('app_table_id')); + } + + private function removeAssets(): void + { + $schemaManager = $this->driverConnection->createSchemaManager(); + + if ($schemaManager->tablesExist(['queue_table'])) { + $schemaManager->dropTable('queue_table'); + } + + if ($schemaManager->tablesExist(['app_table'])) { + $schemaManager->dropTable('app_table'); + } + + if ($this->hasSequence('app_table_id')) { + $schemaManager->dropSequence('app_table_id'); + } + } + + private function hasSequence(string $name): bool + { + $schemaManager = $this->driverConnection->createSchemaManager(); + + $sequences = $schemaManager->listSequences(); + foreach ($sequences as $sequence) { + if ($sequence->getName() === $name) { + return true; + } + } + + return false; + } +} diff --git a/src/Symfony/Component/Messenger/EventListener/DispatchPcntlSignalListener.php b/src/Symfony/Component/Messenger/EventListener/DispatchPcntlSignalListener.php index 37b88ca1b852a..258ce64480344 100644 --- a/src/Symfony/Component/Messenger/EventListener/DispatchPcntlSignalListener.php +++ b/src/Symfony/Component/Messenger/EventListener/DispatchPcntlSignalListener.php @@ -21,6 +21,10 @@ class DispatchPcntlSignalListener implements EventSubscriberInterface { public function onWorkerRunning(): void { + if (!\function_exists('pcntl_signal_dispatch')) { + return; + } + pcntl_signal_dispatch(); } diff --git a/src/Symfony/Component/Mime/RawMessage.php b/src/Symfony/Component/Mime/RawMessage.php index 4ffa1870c0d8e..4a628d8bafbbb 100644 --- a/src/Symfony/Component/Mime/RawMessage.php +++ b/src/Symfony/Component/Mime/RawMessage.php @@ -18,7 +18,8 @@ */ class RawMessage { - private iterable|string $message; + /** @var iterable|string|resource */ + private $message; private bool $isGeneratorClosed; public function __construct(iterable|string $message) @@ -26,12 +27,23 @@ public function __construct(iterable|string $message) $this->message = $message; } + public function __destruct() + { + if (\is_resource($this->message)) { + fclose($this->message); + } + } + public function toString(): string { if (\is_string($this->message)) { return $this->message; } + if (\is_resource($this->message)) { + return stream_get_contents($this->message, -1, 0); + } + $message = ''; foreach ($this->message as $chunk) { $message .= $chunk; @@ -52,10 +64,19 @@ public function toIterable(): iterable return; } + if (\is_resource($this->message)) { + rewind($this->message); + while ($line = fgets($this->message)) { + yield $line; + } + + return; + } + if ($this->message instanceof \Generator) { - $message = ''; + $message = fopen('php://temp', 'w+'); foreach ($this->message as $chunk) { - $message .= $chunk; + fwrite($message, $chunk); yield $chunk; } $this->isGeneratorClosed = !$this->message->valid(); diff --git a/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php b/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php index 70ab2d7c30a8c..4ac1fa5aab631 100644 --- a/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php +++ b/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php @@ -143,15 +143,15 @@ public function testFromPathWithUrl() } $finder = new PhpExecutableFinder(); - $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', 'localhost:8057'])); + $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', 'localhost:8856'])); $process->setWorkingDirectory(__DIR__.'/../Fixtures/web'); $process->start(); try { do { usleep(50000); - } while (!@fopen('http://localhost:8057', 'r')); - $p = DataPart::fromPath($file = 'http://localhost:8057/logo_symfony_header.png'); + } while (!@fopen('http://localhost:8856', 'r')); + $p = DataPart::fromPath($file = 'http://localhost:8856/logo_symfony_header.png'); $content = file_get_contents($file); $this->assertEquals($content, $p->getBody()); $maxLineLength = 76; diff --git a/src/Symfony/Component/Mime/composer.json b/src/Symfony/Component/Mime/composer.json index 2ade1c61d3934..5304bdf36d90b 100644 --- a/src/Symfony/Component/Mime/composer.json +++ b/src/Symfony/Component/Mime/composer.json @@ -28,14 +28,14 @@ "symfony/process": "^6.4|^7.0", "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0" + "symfony/serializer": "^6.4.3|^7.0.3" }, "conflict": { "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", "symfony/mailer": "<6.4", - "symfony/serializer": "<6.4" + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "autoload": { "psr-4": { "Symfony\\Component\\Mime\\": "" }, diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php b/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php index 65321f79ca793..75621994d2c2e 100644 --- a/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php @@ -63,7 +63,7 @@ protected function doSend(MessageInterface $message): SentMessage $options = []; $options['from'] = $message->getFrom() ?: $this->from; - $options['to'] = $message->getPhone(); + $options['to'] = [$message->getPhone()]; $options['text'] = $message->getSubject(); $response = $this->client->request('POST', $endpoint, [ diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php index 944c5d2c46ec0..0cb81e6ef594b 100644 --- a/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php @@ -91,15 +91,24 @@ protected function doSend(MessageInterface $message): SentMessage $threadKey = $options->getThreadKey() ?: $this->threadKey; + $threadKey = $options->getThreadKey() ?: $this->threadKey; + $url = sprintf('https://%s/v1/spaces/%s/messages?key=%s&token=%s%s', $this->getEndpoint(), $this->space, urlencode($this->accessKey), urlencode($this->accessToken), - $threadKey ? '&threadKey='.urlencode($threadKey) : '' + $threadKey ? '&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD' : '' ); + + $body = array_filter($options->toArray()); + + if ($threadKey) { + $body['thread']['threadKey'] = $threadKey; + } + $response = $this->client->request('POST', $url, [ - 'json' => array_filter($options->toArray()), + 'json' => $body, ]); try { diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php index 4f423290c3608..2b277f910b0b9 100644 --- a/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php @@ -108,11 +108,11 @@ public function testSendWithOptions() ->method('getContent') ->willReturn('{"name":"spaces/My-Space/messages/abcdefg.hijklmno"}'); - $expectedBody = json_encode(['text' => $message]); + $expectedBody = json_encode(['text' => $message, 'thread' => ['threadKey' => 'My-Thread']]); $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { $this->assertSame('POST', $method); - $this->assertSame('https://chat.googleapis.com/v1/spaces/My-Space/messages?key=theAccessKey&token=theAccessToken%3D&threadKey=My-Thread', $url); + $this->assertSame('https://chat.googleapis.com/v1/spaces/My-Space/messages?key=theAccessKey&token=theAccessToken%3D&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD', $url); $this->assertSame($expectedBody, $options['body']); return $response; diff --git a/src/Symfony/Component/Notifier/ChatterInterface.php b/src/Symfony/Component/Notifier/ChatterInterface.php index 915190e623aaa..6d89ca921e970 100644 --- a/src/Symfony/Component/Notifier/ChatterInterface.php +++ b/src/Symfony/Component/Notifier/ChatterInterface.php @@ -14,7 +14,7 @@ use Symfony\Component\Notifier\Transport\TransportInterface; /** - * Interface for classes able to send chat messages synchronous and/or asynchronous. + * Interface for classes able to send chat messages synchronously and/or asynchronously. * * @author Fabien Potencier */ diff --git a/src/Symfony/Component/Notifier/TexterInterface.php b/src/Symfony/Component/Notifier/TexterInterface.php index e65547755cd70..a044bb6d5d835 100644 --- a/src/Symfony/Component/Notifier/TexterInterface.php +++ b/src/Symfony/Component/Notifier/TexterInterface.php @@ -14,7 +14,7 @@ use Symfony\Component\Notifier\Transport\TransportInterface; /** - * Interface for classes able to send SMS messages synchronous and/or asynchronous. + * Interface for classes able to send SMS messages synchronously and/or asynchronously. * * @author Fabien Potencier */ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 030b0360c06af..868696209eda3 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -398,6 +398,11 @@ public function testUnknownPseudoType() $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'scalar')], $this->extractor->getTypes(PseudoTypeDummy::class, 'unknownPseudoType')); } + public function testGenericInterface() + { + $this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface')); + } + /** * @dataProvider constructorTypesProvider */ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 74ef738ab2dde..73ce271e9577f 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -385,7 +385,7 @@ public static function unionTypesProvider(): array ['b', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)])]], ['c', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)])]], ['d', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING)])])]], - ['e', [new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class, true, [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING)])], [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_STRING, false, null, true, [], [new Type(Type::BUILTIN_TYPE_OBJECT, false, DefaultValue::class)])])]), new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], + ['e', [new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class, false, [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING)])], [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_OBJECT, false, \Traversable::class, true, [], [new Type(Type::BUILTIN_TYPE_OBJECT, false, DefaultValue::class)])])]), new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], ['f', null], ['g', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)])]], ]; @@ -477,6 +477,11 @@ public static function php80TypesProvider() [Php80PromotedDummy::class, 'promoted', null], ]; } + + public function testGenericInterface() + { + $this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface')); + } } class PhpStanOmittedParamTagTypeDocBlock diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index c61a646612423..599b366db1715 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -68,6 +68,7 @@ public function testGetProperties() 'arrayOfMixed', 'listOfStrings', 'parentAnnotation', + 'genericInterface', 'foo', 'foo2', 'foo3', @@ -132,6 +133,7 @@ public function testGetPropertiesWithCustomPrefixes() 'arrayOfMixed', 'listOfStrings', 'parentAnnotation', + 'genericInterface', 'foo', 'foo2', 'foo3', @@ -185,6 +187,7 @@ public function testGetPropertiesWithNoPrefixes() 'arrayOfMixed', 'listOfStrings', 'parentAnnotation', + 'genericInterface', 'foo', 'foo2', 'foo3', diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index c76f7a7c8b296..1478c87f551b6 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -165,6 +165,11 @@ class Dummy extends ParentDummy */ public $parentAnnotation; + /** + * @var \BackedEnum + */ + public $genericInterface; + public static function getStatic() { } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyUnionType.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyUnionType.php index 86ddb8a1650eb..7e2e1aa3ec8f7 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyUnionType.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyUnionType.php @@ -40,7 +40,7 @@ class DummyUnionType public $d; /** - * @var (Dummy, (int | (string)[])> | ParentDummy | null) + * @var (Dummy, (int | (\Traversable)[])> | ParentDummy | null) */ public $e; diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index b77da7d3f1ffe..bde63b89f67f2 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -105,9 +105,9 @@ public function getTypes(DocType $varType): array /** * Creates a {@see Type} from a PHPDoc type. */ - private function createType(DocType $type, bool $nullable, ?string $docType = null): ?Type + private function createType(DocType $type, bool $nullable): ?Type { - $docType ??= (string) $type; + $docType = (string) $type; if ($type instanceof Collection) { $fqsen = $type->getFqsen(); @@ -118,10 +118,17 @@ private function createType(DocType $type, bool $nullable, ?string $docType = nu [$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen); + $collection = \is_a($class, \Traversable::class, true) || \is_a($class, \ArrayAccess::class, true); + + // it's safer to fall back to other extractors if the generic type is too abstract + if (!$collection && !class_exists($class)) { + return null; + } + $keys = $this->getTypes($type->getKeyType()); $values = $this->getTypes($type->getValueType()); - return new Type($phpType, $nullable, $class, true, $keys, $values); + return new Type($phpType, $nullable, $class, $collection, $keys, $values); } // Cannot guess diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php index 0a02071ec70b7..f8cc166f3be99 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php @@ -125,6 +125,13 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array return [$mainType]; } + $collection = $mainType->isCollection() || \in_array($mainType->getClassName(), [\Traversable::class, \Iterator::class, \IteratorAggregate::class, \ArrayAccess::class, \Generator::class], true); + + // it's safer to fall back to other extractors if the generic type is too abstract + if (!$collection && !class_exists($mainType->getClassName())) { + return []; + } + $collectionKeyTypes = $mainType->getCollectionKeyTypes(); $collectionKeyValues = []; if (1 === \count($node->genericTypes)) { @@ -140,7 +147,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array } } - return [new Type($mainType->getBuiltinType(), $mainType->isNullable(), $mainType->getClassName(), true, $collectionKeyTypes, $collectionKeyValues)]; + return [new Type($mainType->getBuiltinType(), $mainType->isNullable(), $mainType->getClassName(), $collection, $collectionKeyTypes, $collectionKeyValues)]; } if ($node instanceof ArrayShapeNode) { return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]; diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf index 6d7dc7fc23e33..93ff24f330735 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf @@ -64,7 +64,7 @@ Too many failed login attempts, please try again later. - Massa intents d'inici de sessió fallits, torneu-ho a provar més tard. + Massa intents d'inici de sessió fallits, si us plau torneu-ho a provar més tard. Invalid or expired login link. @@ -72,11 +72,11 @@ Too many failed login attempts, please try again in %minutes% minute. - Massa intents d'inici de sessió fallits, torneu-ho a provar en %minutes% minut. + Massa intents d'inici de sessió fallits, si us plau torneu-ho a provar en %minutes% minut. Too many failed login attempts, please try again in %minutes% minutes. - Massa intents fallits d'inici de sessió, torneu-ho a provar d'aquí a %minutes% minuts. + Massa intents d'inici de sessió fallits, si us plau torneu-ho a provar en %minutes% minuts. diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index 06335cbf730e9..7cfd8afbe40bf 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -87,7 +87,7 @@ public function authenticate(RequestEvent $event): void } $request = $event->getRequest(); - $session = $request->hasPreviousSession() ? $request->getSession() : null; + $session = !$request->attributes->getBoolean('_stateless') && $request->hasPreviousSession() ? $request->getSession() : null; $request->attributes->set('_security_firewall_run', $this->sessionKey); diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 9400616bddcd2..88056e677522c 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -552,8 +552,22 @@ private function validateAndDenormalize(array $types, string $currentClass, stri return $data; } - if (('is_'.$builtinType)($data)) { - return $data; + switch ($builtinType) { + case Type::BUILTIN_TYPE_ARRAY: + case Type::BUILTIN_TYPE_BOOL: + case Type::BUILTIN_TYPE_CALLABLE: + case Type::BUILTIN_TYPE_FLOAT: + case Type::BUILTIN_TYPE_INT: + case Type::BUILTIN_TYPE_ITERABLE: + case Type::BUILTIN_TYPE_NULL: + case Type::BUILTIN_TYPE_OBJECT: + case Type::BUILTIN_TYPE_RESOURCE: + case Type::BUILTIN_TYPE_STRING: + if (('is_'.$builtinType)($data)) { + return $data; + } + + break; } } catch (NotNormalizableValueException|InvalidArgumentException $e) { if (!$isUnionType && !$isNullable) { diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index 1b51b729c660b..1f19bd9d86533 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -177,15 +177,19 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string if ($context['_read_attributes'] ?? true) { if (!isset(self::$isReadableCache[$class.$attribute])) { - self::$isReadableCache[$class.$attribute] = $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute); + self::$isReadableCache[$class.$attribute] = (\is_object($classOrObject) && $this->propertyAccessor->isReadable($classOrObject, $attribute)) || $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute); } return self::$isReadableCache[$class.$attribute]; } if (!isset(self::$isWritableCache[$class.$attribute])) { - self::$isWritableCache[$class.$attribute] = $this->propertyInfoExtractor->isWritable($class, $attribute) - || (($writeInfo = $this->writeInfoExtractor->getWriteInfo($class, $attribute)) && PropertyWriteInfo::TYPE_NONE !== $writeInfo->getType()); + if (str_contains($attribute, '.')) { + self::$isWritableCache[$class.$attribute] = true; + } else { + self::$isWritableCache[$class.$attribute] = $this->propertyInfoExtractor->isWritable($class, $attribute) + || (($writeInfo = $this->writeInfoExtractor->getWriteInfo($class, $attribute)) && PropertyWriteInfo::TYPE_NONE !== $writeInfo->getType()); + } } return self::$isWritableCache[$class.$attribute]; diff --git a/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php b/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php index 8da1b471bd567..57f2b568ef44e 100644 --- a/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php +++ b/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php @@ -156,7 +156,7 @@ class ZooWithKeyTypes public $animalsString = []; /** @var array */ public $animalsUnion = []; - /** @var \stdClass */ + /** @var \Traversable */ public $animalsGenerics = []; } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php index 41da0eac8a999..ef7a8c906dc51 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php @@ -26,6 +26,6 @@ public function __construct() public function denormalize(DenormalizerInterface $denormalizer, $data, ?string $format = null, array $context = []): void { - throw new NotNormalizableValueException(); + throw new NotNormalizableValueException('Custom exception message'); } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/property-path-mapping.yaml b/src/Symfony/Component/Serializer/Tests/Fixtures/property-path-mapping.yaml new file mode 100644 index 0000000000000..834b39150fe89 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/property-path-mapping.yaml @@ -0,0 +1,5 @@ +Symfony\Component\Serializer\Tests\Normalizer\ObjectOuter: + attributes: + inner.foo: + serialized_name: inner_foo + groups: [ 'read' ] diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index d809b16c215b5..1809b967c55ff 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -893,6 +893,7 @@ public function testDenormalizeUntypedFormat() public function testDenormalizeUntypedFormatNotNormalizable() { $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('Custom exception message'); $serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); $serializer->denormalize(['value' => 'test'], DummyWithNotNormalizable::class, 'xml'); } @@ -1076,6 +1077,26 @@ public function testNormalizationWithMaxDepthOnStdclassObjectDoesNotThrowWarning $this->assertSame(['string' => 'yes'], $normalized); } + + /** + * @dataProvider provideBooleanTypesData + */ + public function testDenormalizeBooleanTypesWithNotMatchingData(array $data, string $type) + { + $normalizer = new AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors(); + + $this->expectException(NotNormalizableValueException::class); + + $normalizer->denormalize($data, $type); + } + + public function provideBooleanTypesData() + { + return [ + [['foo' => true], FalsePropertyDummy::class], + [['foo' => false], TruePropertyDummy::class], + ]; + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer @@ -1344,6 +1365,18 @@ class XmlScalarDummy public $value; } +class FalsePropertyDummy +{ + /** @var false */ + public $foo; +} + +class TruePropertyDummy +{ + /** @var true */ + public $foo; +} + class SerializerCollectionDummy implements SerializerInterface, DenormalizerInterface { private array $normalizers; @@ -1487,3 +1520,33 @@ public function __construct( ) { } } + +class AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors extends AbstractObjectNormalizer +{ + public function __construct() + { + parent::__construct(new ClassMetadataFactory(new AttributeLoader()), null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()])); + } + + public function getSupportedTypes(?string $format): array + { + return ['*' => false]; + } + + protected function extractAttributes(object $object, ?string $format = null, array $context = []): array + { + return []; + } + + protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed + { + return null; + } + + protected function setAttributeValue(object $object, string $attribute, $value, ?string $format = null, array $context = []): void + { + if (property_exists($object, $attribute)) { + $object->$attribute = $value; + } + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 65452bc2b8afc..70a342c27d55f 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -25,6 +25,7 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; @@ -887,6 +888,40 @@ public function testDenormalizeWithIgnoreAttributeAndPrivateProperties() $this->assertEquals($expected, $obj); } + + public function testNormalizeWithPropertyPath() + { + $classMetadataFactory = new ClassMetadataFactory(new YamlFileLoader(__DIR__.'/../Fixtures/property-path-mapping.yaml')); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $dummyInner = new ObjectInner(); + $dummyInner->foo = 'foo'; + $dummy = new ObjectOuter(); + $dummy->setInner($dummyInner); + + $this->assertSame(['inner_foo' => 'foo'], $normalizer->normalize($dummy, 'json', ['groups' => 'read'])); + } + + public function testDenormalizeWithPropertyPath() + { + $classMetadataFactory = new ClassMetadataFactory(new YamlFileLoader(__DIR__.'/../Fixtures/property-path-mapping.yaml')); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $dummy = new ObjectOuter(); + $dummy->setInner(new ObjectInner()); + + $obj = $normalizer->denormalize(['inner_foo' => 'foo'], ObjectOuter::class, 'json', [ + 'object_to_populate' => $dummy, + 'groups' => 'read', + ]); + + $expectedInner = new ObjectInner(); + $expectedInner->foo = 'foo'; + $expected = new ObjectOuter(); + $expected->setInner($expectedInner); + + $this->assertEquals($expected, $obj); + } } class ProxyObjectDummy extends ObjectDummy diff --git a/src/Symfony/Component/String/AbstractUnicodeString.php b/src/Symfony/Component/String/AbstractUnicodeString.php index bbb005d6ca1fe..4e0850108cb42 100644 --- a/src/Symfony/Component/String/AbstractUnicodeString.php +++ b/src/Symfony/Component/String/AbstractUnicodeString.php @@ -346,8 +346,8 @@ public function reverse(): static public function snake(): static { - $str = $this->camel(); - $str->string = mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str->string), 'UTF-8'); + $str = clone $this; + $str->string = str_replace(' ', '_', mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1 \2', $str->string), 'UTF-8')); return $str; } diff --git a/src/Symfony/Component/String/ByteString.php b/src/Symfony/Component/String/ByteString.php index 3ebe43c10a64a..6389dbd14cc7b 100644 --- a/src/Symfony/Component/String/ByteString.php +++ b/src/Symfony/Component/String/ByteString.php @@ -342,8 +342,8 @@ public function slice(int $start = 0, ?int $length = null): static public function snake(): static { - $str = $this->camel(); - $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string)); + $str = clone $this; + $str->string = str_replace(' ', '_', strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1 \2', $str->string))); return $str; } diff --git a/src/Symfony/Component/String/Inflector/EnglishInflector.php b/src/Symfony/Component/String/Inflector/EnglishInflector.php index 7027ad3842a5e..77ebc134a436f 100644 --- a/src/Symfony/Component/String/Inflector/EnglishInflector.php +++ b/src/Symfony/Component/String/Inflector/EnglishInflector.php @@ -25,8 +25,32 @@ final class EnglishInflector implements InflectorInterface // Fourth entry: Whether the suffix may succeed a consonant // Fifth entry: singular suffix, normal - // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) - ['a', 1, true, true, ['on', 'um']], + // bacteria (bacterium) + ['airetcab', 8, true, true, 'bacterium'], + + // corpora (corpus) + ['aroproc', 7, true, true, 'corpus'], + + // criteria (criterion) + ['airetirc', 8, true, true, 'criterion'], + + // curricula (curriculum) + ['alucirruc', 9, true, true, 'curriculum'], + + // genera (genus) + ['areneg', 6, true, true, 'genus'], + + // media (medium) + ['aidem', 5, true, true, 'medium'], + + // memoranda (memorandum) + ['adnaromem', 9, true, true, 'memorandum'], + + // phenomena (phenomenon) + ['anemonehp', 9, true, true, 'phenomenon'], + + // strata (stratum) + ['atarts', 6, true, true, 'stratum'], // nebulae (nebula) ['ea', 2, true, true, 'a'], @@ -141,7 +165,7 @@ final class EnglishInflector implements InflectorInterface // shoes (shoe) ['se', 2, true, true, ['', 'e']], - // status (status) + // status (status) ['sutats', 6, true, true, 'status'], // tags (tag) @@ -241,7 +265,7 @@ final class EnglishInflector implements InflectorInterface // albums (album) ['mubla', 5, true, true, 'albums'], - // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) + // bacteria (bacterium), curricula (curriculum), media (medium), memoranda (memorandum), phenomena (phenomenon), strata (stratum) ['mu', 2, true, true, 'a'], // men (man), women (woman) @@ -250,20 +274,11 @@ final class EnglishInflector implements InflectorInterface // people (person) ['nosrep', 6, true, true, ['persons', 'people']], - // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) - ['noi', 3, true, true, 'ions'], - - // coupon (coupons) - ['nop', 3, true, true, 'pons'], - - // seasons (season), treasons (treason), poisons (poison), lessons (lesson) - ['nos', 3, true, true, 'sons'], + // criteria (criterion) + ['noiretirc', 9, true, true, 'criteria'], - // icons (icon) - ['noc', 3, true, true, 'cons'], - - // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) - ['no', 2, true, true, 'a'], + // phenomena (phenomenon) + ['nonemonehp', 10, true, true, 'phenomena'], // echoes (echo) ['ohce', 4, true, true, 'echoes'], @@ -274,6 +289,9 @@ final class EnglishInflector implements InflectorInterface // atlases (atlas) ['salta', 5, true, true, 'atlases'], + // aliases (alias) + ['saila', 5, true, true, 'aliases'], + // irises (iris) ['siri', 4, true, true, 'irises'], @@ -399,6 +417,9 @@ final class EnglishInflector implements InflectorInterface // aircraft 'tfarcria', + + // hardware + 'erawdrah', ]; public function singularize(string $plural): array diff --git a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php index eabe7b912bed5..f8b0509ffa185 100644 --- a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php @@ -1075,6 +1075,8 @@ public static function provideSnake() ['symfony_is_great', 'symfonyIsGREAT'], ['symfony_is_really_great', 'symfonyIsREALLYGreat'], ['symfony', 'SYMFONY'], + ['symfony_is_great', 'SYMFONY IS GREAT'], + ['symfony_is_great', 'SYMFONY_IS_GREAT'], ]; } diff --git a/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php b/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php index ba8d6d797c4d0..fb5d04300305a 100644 --- a/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php +++ b/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php @@ -35,7 +35,7 @@ public static function singularizeProvider() ['atlases', ['atlas', 'atlase', 'atlasis']], ['axes', ['ax', 'axe', 'axis']], ['babies', 'baby'], - ['bacteria', ['bacterion', 'bacterium']], + ['bacteria', 'bacterium'], ['bases', ['bas', 'base', 'basis']], ['batches', ['batch', 'batche']], ['beaux', 'beau'], @@ -46,6 +46,7 @@ public static function singularizeProvider() ['bureaux', 'bureau'], ['buses', ['bus', 'buse', 'busis']], ['bushes', ['bush', 'bushe']], + ['buttons', 'button'], ['calves', ['calf', 'calve', 'calff']], ['cars', 'car'], ['cassettes', ['cassett', 'cassette']], @@ -57,10 +58,12 @@ public static function singularizeProvider() ['cliffs', 'cliff'], ['codes', 'code'], ['committee', 'committee'], + ['corpora', 'corpus'], + ['coupons', 'coupon'], ['crises', ['cris', 'crise', 'crisis']], - ['criteria', ['criterion', 'criterium']], + ['criteria', 'criterion'], ['cups', 'cup'], - ['coupons', 'coupon'], + ['curricula', 'curriculum'], ['data', 'data'], ['days', 'day'], ['discos', 'disco'], @@ -86,6 +89,7 @@ public static function singularizeProvider() ['funguses', ['fungus', 'funguse', 'fungusis']], ['garages', ['garag', 'garage']], ['geese', 'goose'], + ['genera', 'genus'], ['halves', ['half', 'halve', 'halff']], ['hats', 'hat'], ['heroes', ['hero', 'heroe']], @@ -106,6 +110,8 @@ public static function singularizeProvider() ['lives', 'life'], ['matrices', ['matrex', 'matrix', 'matrice']], ['matrixes', 'matrix'], + ['media', 'medium'], + ['memoranda', 'memorandum'], ['men', 'man'], ['mice', 'mouse'], ['moves', 'move'], @@ -120,7 +126,7 @@ public static function singularizeProvider() ['parties', 'party'], ['people', 'person'], ['persons', 'person'], - ['phenomena', ['phenomenon', 'phenomenum']], + ['phenomena', 'phenomenon'], ['photos', 'photo'], ['pianos', 'piano'], ['plateaux', 'plateau'], @@ -146,7 +152,7 @@ public static function singularizeProvider() ['status', 'status'], ['statuses', 'status'], ['stories', 'story'], - ['strata', ['straton', 'stratum']], + ['strata', 'stratum'], ['suitcases', ['suitcas', 'suitcase', 'suitcasis']], ['syllabi', 'syllabus'], ['tags', 'tag'], @@ -200,7 +206,9 @@ public static function pluralizeProvider() ['bureau', ['bureaus', 'bureaux']], ['bus', 'buses'], ['bush', 'bushes'], + ['button', 'buttons'], ['calf', ['calfs', 'calves']], + ['campus', 'campuses'], ['car', 'cars'], ['cassette', 'cassettes'], ['cave', 'caves'], @@ -210,10 +218,11 @@ public static function pluralizeProvider() ['circus', 'circuses'], ['cliff', 'cliffs'], ['committee', 'committees'], + ['coupon', 'coupons'], ['crisis', 'crises'], - ['criteria', 'criterion'], + ['criterion', 'criteria'], ['cup', 'cups'], - ['coupon', 'coupons'], + ['curriculum', 'curricula'], ['data', 'data'], ['day', 'days'], ['disco', 'discos'], @@ -237,10 +246,12 @@ public static function pluralizeProvider() ['half', ['halfs', 'halves']], ['hat', 'hats'], ['hero', 'heroes'], + ['hippocampus', 'hippocampi'], ['hippopotamus', 'hippopotami'], // hippopotamuses ['hoax', 'hoaxes'], ['hoof', ['hoofs', 'hooves']], ['house', 'houses'], + ['icon', 'icons'], ['index', ['indicies', 'indexes']], ['ion', 'ions'], ['iris', 'irises'], @@ -253,6 +264,8 @@ public static function pluralizeProvider() ['louse', 'lice'], ['man', 'men'], ['matrix', ['matricies', 'matrixes']], + ['medium', 'media'], + ['memorandum', 'memoranda'], ['mouse', 'mice'], ['move', 'moves'], ['movie', 'movies'], @@ -286,6 +299,7 @@ public static function pluralizeProvider() ['shoe', 'shoes'], ['species', 'species'], ['status', ['status', 'statuses']], + ['stratum', 'strata'], ['spy', 'spies'], ['staff', 'staves'], ['story', 'stories'], @@ -302,6 +316,8 @@ public static function pluralizeProvider() ['icon', 'icons'], ['hippocampus', 'hippocampi'], ['campus', 'campuses'], + ['hardware', 'hardware'], + ['alias', 'aliases'], // test casing: if the first letter was uppercase, it should remain so ['Man', 'Men'], diff --git a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php index e4e9ddbe5abd0..81533272d19de 100644 --- a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php @@ -48,7 +48,7 @@ public function validate(mixed $value, Constraint $constraint): void if (\in_array($element, $collectionElements, true)) { $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameter('{{ value }}', $this->formatValue($element)) ->setCode(Unique::IS_NOT_UNIQUE) ->addViolation(); diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf index 3d0b819a95441..3c0ace5490efd 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf @@ -440,7 +440,7 @@ This URL is missing a top-level domain. - Acest URL îi lipsește un domeniu de nivel superior. + Acestui URL îi lipsește un domeniu de nivel superior. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php index 5c7904a8001af..6c298f8236791 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php @@ -396,7 +396,7 @@ public static function getInvalidHSL(): array } /** - * @dataProvider getInvalidHSL + * @dataProvider getInvalidHSLA */ public function testInvalidHSLA($cssColor) { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php index 3c2dd9f21c98f..0d5ccdb8e56e2 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php @@ -61,7 +61,7 @@ public static function getValidValues() /** * @dataProvider getInvalidValues */ - public function testInvalidValues($value) + public function testInvalidValues($value, $expectedMessageParam) { $constraint = new Unique([ 'message' => 'myMessage', @@ -69,7 +69,7 @@ public function testInvalidValues($value) $this->validator->validate($value, $constraint); $this->buildViolation('myMessage') - ->setParameter('{{ value }}', 'array') + ->setParameter('{{ value }}', $expectedMessageParam) ->setCode(Unique::IS_NOT_UNIQUE) ->assertRaised(); } @@ -79,12 +79,12 @@ public static function getInvalidValues() $object = new \stdClass(); return [ - yield 'not unique booleans' => [[true, true]], - yield 'not unique integers' => [[1, 2, 3, 3]], - yield 'not unique floats' => [[0.1, 0.2, 0.1]], - yield 'not unique string' => [['a', 'b', 'a']], - yield 'not unique arrays' => [[[1, 1], [2, 3], [1, 1]]], - yield 'not unique objects' => [[$object, $object]], + yield 'not unique booleans' => [[true, true], 'true'], + yield 'not unique integers' => [[1, 2, 3, 3], 3], + yield 'not unique floats' => [[0.1, 0.2, 0.1], 0.1], + yield 'not unique string' => [['a', 'b', 'a'], '"a"'], + yield 'not unique arrays' => [[[1, 1], [2, 3], [1, 1]], 'array'], + yield 'not unique objects' => [[$object, $object], 'object'], ]; } @@ -94,7 +94,7 @@ public function testInvalidValueNamed() $this->validator->validate([1, 2, 3, 3], $constraint); $this->buildViolation('myMessage') - ->setParameter('{{ value }}', 'array') + ->setParameter('{{ value }}', '3') ->setCode(Unique::IS_NOT_UNIQUE) ->assertRaised(); } @@ -174,7 +174,7 @@ public function testExpectsInvalidNonStrictComparison() ])); $this->buildViolation('myMessage') - ->setParameter('{{ value }}', 'array') + ->setParameter('{{ value }}', '1') ->setCode(Unique::IS_NOT_UNIQUE) ->assertRaised(); } @@ -200,7 +200,7 @@ public function testExpectsInvalidCaseInsensitiveComparison() ])); $this->buildViolation('myMessage') - ->setParameter('{{ value }}', 'array') + ->setParameter('{{ value }}', '"hello"') ->setCode(Unique::IS_NOT_UNIQUE) ->assertRaised(); } @@ -246,14 +246,14 @@ public static function getInvalidFieldNames(): array /** * @dataProvider getInvalidCollectionValues */ - public function testInvalidCollectionValues(array $value, array $fields) + public function testInvalidCollectionValues(array $value, array $fields, string $expectedMessageParam) { $this->validator->validate($value, new Unique([ 'message' => 'myMessage', ], fields: $fields)); $this->buildViolation('myMessage') - ->setParameter('{{ value }}', 'array') + ->setParameter('{{ value }}', $expectedMessageParam) ->setCode(Unique::IS_NOT_UNIQUE) ->assertRaised(); } @@ -264,23 +264,25 @@ public static function getInvalidCollectionValues(): array 'unique string' => [[ ['lang' => 'eng', 'translation' => 'hi'], ['lang' => 'eng', 'translation' => 'hello'], - ], ['lang']], + ], ['lang'], 'array'], 'unique floats' => [[ ['latitude' => 51.509865, 'longitude' => -0.118092, 'poi' => 'capital'], ['latitude' => 52.520008, 'longitude' => 13.404954], ['latitude' => 51.509865, 'longitude' => -0.118092], - ], ['latitude', 'longitude']], + ], ['latitude', 'longitude'], 'array'], 'unique int' => [[ ['id' => 1, 'email' => 'bar@email.com'], ['id' => 1, 'email' => 'foo@email.com'], - ], ['id']], + ], ['id'], 'array'], 'unique null' => [ [null, null], [], + 'null', ], 'unique field null' => [ [['nullField' => null], ['nullField' => null]], ['nullField'], + 'array', ], ]; } diff --git a/src/Symfony/Component/VarDumper/Caster/FFICaster.php b/src/Symfony/Component/VarDumper/Caster/FFICaster.php index f1984eef368ee..ffed9f315ba56 100644 --- a/src/Symfony/Component/VarDumper/Caster/FFICaster.php +++ b/src/Symfony/Component/VarDumper/Caster/FFICaster.php @@ -115,11 +115,21 @@ private static function castFFIPointer(Stub $stub, CType $type, ?CData $data = n private static function castFFIStringValue(CData $data): string|CutStub { $result = []; + $ffi = \FFI::cdef(<<zend_get_page_size(); + + // get cdata address + $start = $ffi->cast('uintptr_t', $ffi->cast('char*', $data))->cdata; + // accessing memory in the same page as $start is safe + $max = min(self::MAX_STRING_LENGTH, ($start | ($pageSize - 1)) - $start); + + for ($i = 0; $i < $max; ++$i) { $result[$i] = $data[$i]; - if ("\0" === $result[$i]) { + if ("\0" === $data[$i]) { return implode('', $result); } } diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php index 5e7ec147bbbe6..362e0a2c532c9 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php @@ -24,6 +24,11 @@ class FFICasterTest extends TestCase { use VarDumperTestTrait; + /** + * @see FFICaster::MAX_STRING_LENGTH + */ + private const MAX_STRING_LENGTH = 255; + protected function setUp(): void { if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && 'preload' === \ini_get('ffi.enable')) { @@ -173,17 +178,24 @@ public function testCastCuttedPointerToChar() { $actualMessage = str_repeat('Hello World!', 30)."\0"; $actualLength = \strlen($actualMessage); - - $expectedMessage = 'Hello World!Hello World!Hello World!Hello World!' - .'Hello World!Hello World!Hello World!Hello World!Hello World!Hel' - .'lo World!Hello World!Hello World!Hello World!Hello World!Hello ' - .'World!Hello World!Hello World!Hello World!Hello World!Hello Wor' - .'ld!Hello World!Hel'; + $expectedMessage = substr($actualMessage, 0, self::MAX_STRING_LENGTH); $string = \FFI::cdef()->new('char['.$actualLength.']'); $pointer = \FFI::addr($string[0]); \FFI::memcpy($pointer, $actualMessage, $actualLength); + // the max length is platform-dependent and can be less than 255, + // so we need to cut the expected message to the maximum length + // allowed by pages size of the current system + $ffi = \FFI::cdef(<<zend_get_page_size(); + $start = $ffi->cast('uintptr_t', $ffi->cast('char*', $pointer))->cdata; + $max = min(self::MAX_STRING_LENGTH, ($start | ($pageSize - 1)) - $start); + $expectedMessage = substr($expectedMessage, 0, $max); + $this->assertDumpEquals(<< size 8 align 8 { cdata: "$expectedMessage"… @@ -191,34 +203,21 @@ public function testCastCuttedPointerToChar() PHP, $pointer); } - /** - * It is worth noting that such a test can cause SIGSEGV, as it breaks - * into "foreign" memory. However, this is only theoretical, since - * memory is allocated within the PHP process and almost always "garbage - * data" will be read from the PHP process itself. - * - * If this test fails for some reason, please report it: We may have to - * disable the dumping of strings ("char*") feature in VarDumper. - * - * @see FFICaster::castFFIStringValue() - */ public function testCastNonTrailingCharPointer() { $actualMessage = 'Hello World!'; $actualLength = \strlen($actualMessage); - $string = \FFI::cdef()->new('char['.$actualLength.']'); + $string = \FFI::cdef()->new('char['.($actualLength + 1).']'); $pointer = \FFI::addr($string[0]); - \FFI::memcpy($pointer, $actualMessage, $actualLength); - // Remove automatically addition of the trailing "\0" and remove trailing "\0" $pointer = \FFI::cdef()->cast('char*', \FFI::cdef()->cast('void*', $pointer)); $pointer[$actualLength] = "\x01"; $this->assertDumpMatchesFormat(<< size 8 align 8 { - cdata: "$actualMessage%s" + cdata: %A"$actualMessage%s" } PHP, $pointer); } diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index c5978ee06820f..4cf0f65b4531e 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -197,10 +197,35 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf $body = $methods ? "\n".implode("\n\n", $methods)."\n" : ''; $propertyScopes = $class ? self::exportPropertyScopes($class->name) : '[]'; + if ( + $class?->hasMethod('__unserialize') + && !$class->getMethod('__unserialize')->getParameters()[0]->getType() + ) { + // fix contravariance type problem when $class declares a `__unserialize()` method without typehint. + $lazyProxyTraitStatement = <<__doUnserialize(\$data); + } + + EOPHP; + } else { + $lazyProxyTraitStatement = <<assertSame($expected, ProxyHelper::generateLazyProxy(null, [new \ReflectionClass(TestForProxyHelperInterface1::class), new \ReflectionClass(TestForProxyHelperInterface2::class)])); } + /** + * @dataProvider classWithUnserializeMagicMethodProvider + */ + public function testGenerateLazyProxyForClassWithUnserializeMagicMethod(object $obj, string $expected) + { + $this->assertStringContainsString($expected, ProxyHelper::generateLazyProxy(new \ReflectionClass($obj::class))); + } + + public static function classWithUnserializeMagicMethodProvider(): iterable + { + yield 'not type hinted __unserialize method' => [new class() { + public function __unserialize($array) + { + } + }, <<<'EOPHP' + implements \Symfony\Component\VarExporter\LazyObjectInterface + { + use \Symfony\Component\VarExporter\LazyProxyTrait { + __unserialize as private __doUnserialize; + } + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; + + public function __unserialize($data): void + { + $this->__doUnserialize($data); + } + } + EOPHP]; + + yield 'type hinted __unserialize method' => [new class() { + public function __unserialize(array $array) + { + } + }, <<<'EOPHP' + implements \Symfony\Component\VarExporter\LazyObjectInterface + { + use \Symfony\Component\VarExporter\LazyProxyTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; + } + EOPHP]; + } + public function testAttributes() { $expected = <<<'EOPHP' @@ -182,6 +226,7 @@ public function foo(#[\SensitiveParameter, AnotherAttribute] $a): int { } }); + $this->assertStringContainsString($expected, ProxyHelper::generateLazyProxy($class)); }