diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 58caff2209f37..00a686580d01f 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -14,8 +14,9 @@ This will help reviewers and should be a good start for the documentation.
Additionally (see https://symfony.com/releases):
- Always add tests and ensure they pass.
- Bug fixes must be submitted against the lowest maintained branch where they apply
- (lowest branches are regularly merged to upper ones so they get the fixes too.)
+ (lowest branches are regularly merged to upper ones so they get the fixes too).
- Features and deprecations must be submitted against the latest branch.
+ - For new features, provide some code snippets to help understand usage.
- Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry
- Never break backward compatibility (see https://symfony.com/bc).
-->
diff --git a/.github/expected-missing-return-types.diff b/.github/expected-missing-return-types.diff
index fe0381a0b0a7e..cf4c237e3070c 100644
--- a/.github/expected-missing-return-types.diff
+++ b/.github/expected-missing-return-types.diff
@@ -899,11 +899,11 @@ index f38069e471..0966eb3e89 100644
+ public function decode(string $data, string $format, array $context = []): mixed;
/**
-@@ -45,4 +45,4 @@ interface DecoderInterface
+@@ -44,4 +44,4 @@ interface DecoderInterface
* @return bool
*/
-- public function supportsDecoding(string $format /* , array $context = [] */);
-+ public function supportsDecoding(string $format /* , array $context = [] */): bool;
+- public function supportsDecoding(string $format);
++ public function supportsDecoding(string $format): bool;
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
index 44ba45f581..3398115497 100644
diff --git a/CHANGELOG-6.0.md b/CHANGELOG-6.0.md
index 4416525001ff2..54219602ca6bb 100644
--- a/CHANGELOG-6.0.md
+++ b/CHANGELOG-6.0.md
@@ -7,6 +7,44 @@ in 6.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/v6.0.0...v6.0.1
+* 6.0.11 (2022-07-29)
+
+ * bug #47069 [Security] Allow redirect after login to absolute URLs (Tim Ward)
+ * bug #47073 [HttpKernel] Fix non-scalar check in surrogate fragment renderer (aschempp)
+ * bug #47003 [Cache] Ensured that redis adapter can use multiple redis sentinel hosts (warslett)
+ * bug #43329 [Serializer] Respect default context in DateTimeNormalizer::denormalize (hultberg)
+ * bug #47070 [Messenger] Fix function name in TriggerSql on postgresql bridge to support table name with schema (zimny9932)
+ * bug #47086 Workaround disabled "var_dump" (nicolas-grekas)
+ * bug #40828 [BrowserKit] Merge fields and files recursively if they are multidimensional array (januszmk)
+ * bug #47010 [String] Fix `width` method in `AbstractUnicodeString` (TBoileau)
+ * bug #47048 [Serializer] Fix XmlEncoder encoding attribute false (alamirault)
+ * bug #46957 [HttpFoundation] Fix `\Stringable` support in `InputBag::get()` (chalasr)
+ * bug #47022 [Console] get full command path for command in search path (remicollet)
+ * bug #47000 [ErrorHandler] Fix return type patching for list and class-string pseudo types (derrabus)
+ * bug #43998 [HttpKernel] [HttpCache] Don't throw on 304 Not Modified (aleho)
+ * bug #46792 [Bridge] Corrects bug in test listener trait (magikid)
+ * bug #46985 [DoctrineBridge] Avoid calling `AbstractPlatform::hasNativeGuidType()` (derrabus)
+ * bug #46958 [Serializer] Ignore getter with required parameters (Fix #46592) (astepin)
+ * bug #46981 [Mime] quote address names if they contain parentheses (xabbuh)
+ * bug #46960 [FrameworkBundle] Fail gracefully when forms use disabled CSRF (HeahDude)
+ * bug #46973 [DependencyInjection] Fail gracefully when attempting to autowire composite types (derrabus)
+ * bug #45884 [Serializer] Fix inconsistent behaviour of nullable objects in key/value arrays (phramz)
+ * bug #46963 [Mime] Fix inline parts when added via attachPart() (fabpot)
+ * bug #46968 [PropertyInfo] Make sure nested composite types do not crash ReflectionExtractor (derrabus)
+ * bug #46931 Flush backend output buffer after closing. (bradjones1)
+ * bug #46947 [Serializer] Prevent that bad Ignore method annotations lead to incorrect results (astepin)
+ * bug #46948 [Validator] : Fix "PHP Warning: Undefined array key 1" in NotCompromisedPasswordValidator (KevinVanSonsbeek)
+ * bug #46905 [BrowserKit] fix sending request to paths containing multiple slashes (xabbuh)
+ * bug #46244 [Validator] Fix traverse option on Valid constraint when used as Attribute (tobias-93)
+ * bug #42033 [HttpFoundation] Fix deleteFileAfterSend on client abortion (nerg4l)
+ * bug #46941 [Messenger] Fix calls to deprecated DBAL methods (derrabus)
+ * bug #46863 [Mime] Fix invalid DKIM signature with multiple parts (BrokenSourceCode)
+ * bug #46808 [HttpFoundation] Fix TypeError on null `$_SESSION` in `NativeSessionStorage::save()` (chalasr)
+ * bug #46811 [DoctrineBridge] Fix comment for type on Query::setValue (middlewares) (l-vo)
+ * bug #46790 [HttpFoundation] Prevent PHP Warning: Session ID is too long or contains illegal characters (BrokenSourceCode)
+ * bug #46800 Spaces in system temp folder path cause deprecation errors in php 8 (demeritcowboy)
+ * bug #46797 [Messenger] Ceil waiting time when multiplier is a float on retry (WissameMekhilef)
+
* 6.0.10 (2022-06-26)
* bug #46779 [String] Add an invariable word in french (lemonlab)
diff --git a/CHANGELOG-6.1.md b/CHANGELOG-6.1.md
index c8755b2eab6fb..0b83806b53047 100644
--- a/CHANGELOG-6.1.md
+++ b/CHANGELOG-6.1.md
@@ -7,6 +7,45 @@ in 6.1 minor versions.
To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash
To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v6.1.0...v6.1.1
+* 6.1.4 (2022-08-26)
+
+ * bug #47372 [Console] Fix OutputFormatterStyleStack::getCurrent return type (alamirault)
+ * bug #47391 [LokaliseBridge] Fix push command --delete-missing options when there are no missing messages (rwionczek)
+ * bug #47368 [Security] Count remember me cookie parts before accessing the second (MatTheCat)
+ * bug #47358 Fix broken request stack state if throwable is thrown. (Warxcell)
+ * bug #47304 [Serializer] Fix caching context-aware encoders/decoders in ChainEncoder/ChainDecoder (Guite)
+ * bug #47150 [Serializer] Revert deprecation of `ContextAwareEncoderInterface` and `ContextAwareDecoderInterface` (nicolas-grekas)
+ * bug #47329 Email image parts: regex for single closing quote (rr-it)
+ * bug #47335 [Security] [AbstractToken] getUserIdentifier() must return a string (mpiot)
+ * bug #47283 [HttpFoundation] Prevent accepted rate limits with no remaining token to be preferred over denied ones (MatTheCat)
+ * bug #47128 [Serializer] Throw InvalidArgumentException if the data needed in the constructor doesn't belong to a backedEnum (allison guilhem)
+ * bug #47273 [HttpFoundation] Do not send Set-Cookie header twice for deleted session cookie (X-Coder264)
+ * bug #47255 [Serializer] Fix get accessor regex in AnnotationLoader (jsor)
+ * bug #47238 [HttpKernel] Fix passing `null` to `\trim()` method in LoggerDataCollector (SVillette)
+ * bug #47216 [Translation] Crowdin provider throw Exception when status is 50x (alamirault)
+ * bug #47209 Always attempt to listen for notifications (goetas)
+ * bug #47211 [Validator] validate nested constraints only if they are in the same group (xabbuh)
+ * bug #47218 [Console] fix dispatch signal event check for compatibility with the contract interface (xabbuh)
+ * bug #47200 [Form] ignore missing keys when mapping DateTime objects to uninitialized arrays (xabbuh)
+ * bug #47189 [Validator] Add additional hint when `egulias/email-validator` needs to be installed (mpdude)
+ * bug #47195 [FrameworkBundle] fix writes to static $kernel property (xabbuh)
+ * bug #47185 [String] Fix snake conversion (simPod)
+ * bug #47175 [DowCrawler] Fix locale-sensitivity of whitespace normalization (nicolas-grekas)
+ * bug #47172 [Translation] Fix reading intl-icu domains with LocoProvider (nicolas-grekas)
+ * bug #47171 [TwigBridge] suggest to install the Twig bundle when the required component is already installed (xabbuh)
+ * bug #47169 [Serializer] Fix throwing right exception in ArrayDenormalizer with invalid type (norkunas)
+ * bug #47162 [Mailer] Fix error message in case of an SMTP error (fabpot)
+ * bug #47161 [Mailer] Fix logic (fabpot)
+ * bug #47157 [Messenger] Fix Doctrine transport on MySQL (nicolas-grekas)
+ * bug #47155 [Filesystem] Remove needless `mb_*` calls (HellFirePvP)
+ * bug #46190 [Translation] Fix translator overlapse (Xavier RENAUDIN)
+ * bug #47142 [Mailer] Fix error message in case of an STMP error (fabpot)
+ * bug #45333 [Console] Fix ConsoleEvents::SIGNAL subscriber dispatch (GwendolenLynch)
+ * bug #47145 [HttpClient] Fix shared connections not being freed on PHP < 8 (nicolas-grekas)
+ * bug #47143 [HttpClient] Fix memory leak when using StreamWrapper (nicolas-grekas)
+ * bug #47130 [HttpFoundation] Fix invalid ID not regenerated with native PHP file sessions (BrokenSourceCode)
+ * bug #47129 [FrameworkBundle] remove the ChatterInterface alias when the chatter service is removed (xabbuh)
+
* 6.1.3 (2022-07-29)
* bug #47069 [Security] Allow redirect after login to absolute URLs (Tim Ward)
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ec4fddccc86be..99d8c121d9fce 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -21,8 +21,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Jordi Boggiano (seldaek)
- Roland Franssen (ro0)
- Victor Berchet (victor)
- - Tobias Nyholm (tobias)
- Yonel Ceruto (yonelceruto)
+ - Tobias Nyholm (tobias)
- Oskar Stark (oskarstark)
- Ryan Weaver (weaverryan)
- Javier Eguiluz (javier.eguiluz)
@@ -34,35 +34,35 @@ The Symfony Connect username in parenthesis allows to get more information
- Samuel ROZE (sroze)
- Pascal Borreli (pborreli)
- Romain Neutron
- - Joseph Bielawski (stloyd)
- Jules Pietri (heah)
+ - Joseph Bielawski (stloyd)
- Drak (drak)
- Abdellatif Ait boudad (aitboudad)
- Lukas Kahwe Smith (lsmith)
- Jan Schädlich (jschaedl)
- Martin Hasoň (hason)
- - Jeremy Mikola (jmikola)
- Jérôme Tamarelle (gromnan)
+ - Jeremy Mikola (jmikola)
- Jean-François Simon (jfsimon)
- Benjamin Eberlei (beberlei)
- Kevin Bond (kbond)
- Igor Wiedler
- Valentin Udaltsov (vudaltsov)
- Vasilij Duško (staff)
+ - HypeMC (hypemc)
- Matthias Pigulla (mpdude)
- Laurent VOULLEMIER (lvo)
- Pierre du Plessis (pierredup)
- - Grégoire Paris (greg0ire)
- - Jonathan Wage (jwage)
- Antoine Makdessi (amakdessi)
- - HypeMC (hypemc)
+ - Grégoire Paris (greg0ire)
- Gabriel Ostrolucký (gadelat)
+ - Jonathan Wage (jwage)
- David Maicher (dmaicher)
- Titouan Galopin (tgalopin)
- Alexandre Salomé (alexandresalome)
- William DURAND
- - ornicar
- Alexander Schranz (alexander-schranz)
+ - ornicar
- Dany Maillard (maidmaid)
- Eriksen Costa
- Diego Saint Esteben (dosten)
@@ -70,26 +70,26 @@ The Symfony Connect username in parenthesis allows to get more information
- Alexander Mols (asm89)
- Gábor Egyed (1ed)
- Francis Besset (francisbesset)
+ - Alexandre Daubois (alexandre-daubois)
- Vasilij Dusko | CREATION
- - Bulat Shakirzyanov (avalanche123)
- Mathieu Santostefano (welcomattic)
+ - Bulat Shakirzyanov (avalanche123)
- Iltar van der Berg
- - Alexandre Daubois (alexandre-daubois)
- Miha Vrhovnik (mvrhov)
- Saša Stamenković (umpirsky)
- Mathieu Piot (mpiot)
- - Guilhem N (guilhemn)
- Alex Pott
+ - Guilhem N (guilhemn)
- Vladimir Reznichenko (kalessil)
- Sarah Khalil (saro0h)
- Konstantin Kudryashov (everzet)
+ - Vincent Langlet (deviling)
- Bilal Amarni (bamarni)
+ - Tomas Norkūnas (norkunas)
- Eriksen Costa
- Florin Patan (florinpatan)
- Peter Rehm (rpet)
- - Tomas Norkūnas (norkunas)
- Henrik Bjørnskov (henrikbjorn)
- - Vincent Langlet (deviling)
- Konstantin Myakshin (koc)
- Andrej Hudec (pulzarraider)
- Julien Falque (julienfalque)
@@ -104,8 +104,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Fran Moreno (franmomu)
- Jáchym Toušek (enumag)
- Malte Schlüter (maltemaltesich)
- - Vasilij Dusko
- Mathias Arlaud (mtarld)
+ - Vasilij Dusko
- Denis (yethee)
- Arnout Boks (aboks)
- Charles Sarrazin (csarrazi)
@@ -125,14 +125,15 @@ The Symfony Connect username in parenthesis allows to get more information
- Toni Uebernickel (havvg)
- Bart van den Burg (burgov)
- Jordan Alliot (jalliot)
+ - Smaine Milianni (ismail1432)
- John Wards (johnwards)
- Dariusz Ruminski
- Lars Strojny (lstrojny)
- - Smaine Milianni (ismail1432)
+ - Yanick Witschi (toflar)
- Antoine Hérault (herzult)
- Konstantin.Myakshin
+ - Rokas Mikalkėnas (rokasm)
- Arman Hosseini (arman)
- - Yanick Witschi (toflar)
- Arnaud Le Blanc (arnaud-lb)
- Maxime STEINHAUSSER
- Peter Kokot (maastermedia)
@@ -145,22 +146,23 @@ The Symfony Connect username in parenthesis allows to get more information
- YaFou
- Gary PEGEOT (gary-p)
- Chris Wilkinson (thewilkybarkid)
- - Rokas Mikalkėnas (rokasm)
- Brice BERNARD (brikou)
- Roman Martinuk (a2a4)
- Gregor Harlan (gharlan)
+ - Antoine Lamirault
- Baptiste Clavié (talus)
- Adrien Brault (adrienbrault)
- Michal Piotrowski
- marc.weistroff
- lenar
+ - Jesse Rushlow (geeshoe)
- Théo FIDRY
- jeremyFreeAgent (jeremyfreeagent)
- Włodzimierz Gajda (gajdaw)
- Christian Scheb
- Guillaume (guill)
- Mathieu Lechat (mat_the_cat)
- - Jesse Rushlow (geeshoe)
+ - Tugdual Saunier (tucksaun)
- Jacob Dreesen (jdreesen)
- Joel Wurtz (brouznouf)
- Michael Babker (mbabker)
@@ -169,7 +171,6 @@ The Symfony Connect username in parenthesis allows to get more information
- zairig imad (zairigimad)
- Colin Frei
- Javier Spagnoletti (phansys)
- - Tugdual Saunier (tucksaun)
- excelwebzone
- Jérôme Parmentier (lctrs)
- HeahDude
@@ -209,6 +210,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Timo Bakx (timobakx)
- Juti Noppornpitak (shiroyuki)
- Joe Bennett (kralos)
+ - Hugo Alliaume (kocal)
- Anthony MARTIN
- Colin O'Dell (colinodell)
- Sebastian Hörl (blogsh)
@@ -223,16 +225,16 @@ The Symfony Connect username in parenthesis allows to get more information
- Chi-teck
- Guilliam Xavier
- Nate Wiebe (natewiebe13)
- - Hugo Alliaume (kocal)
- Michael Voříšek
- SpacePossum
- - Antoine Lamirault
+ - Andreas Schempp (aschempp)
- Pablo Godel (pgodel)
- Romaric Drigon (romaricdrigon)
- Andréia Bohner (andreia)
- Jannik Zschiesche
- Rafael Dohms (rdohms)
- George Mponos (gmponos)
+ - Fritz Michael Gschwantner (fritzmg)
- Aleksandar Jakovljevic (ajakov)
- jwdeitch
- Jurica Vlahoviček (vjurica)
@@ -243,7 +245,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Farhad Safarov (safarov)
- Jérémy Derussé
- Nicolas Philippe (nikophil)
- - Andreas Schempp (aschempp)
+ - Hubert Lenoir (hubert_lenoir)
+ - Florent Mata (fmata)
- mcfedr (mcfedr)
- Denis Brumann (dbrumann)
- Maciej Malarz (malarzm)
@@ -260,6 +263,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Dmitrii Poddubnyi (karser)
- soyuka
- Rouven Weßling (realityking)
+ - BoShurik
- Zmey
- Clemens Tolboom
- Oleg Voronkovich
@@ -269,6 +273,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Ben Hakim
- Sylvain Fabre (sylfabre)
- Filippo Tessarotto (slamdunk)
+ - Tom Van Looy (tvlooy)
- 77web
- Bohan Yang (brentybh)
- Bastien Jaillot (bastnic)
@@ -279,7 +284,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Dawid Nowak
- Amal Raghav (kertz)
- Jonathan Ingram
- - Fritz Michael Gschwantner (fritzmg)
- Artur Kotyrba
- Tyson Andre
- Thomas Landauer (thomas-landauer)
@@ -291,11 +295,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Sebastien Morel (plopix)
- Sergey (upyx)
- Yoann RENARD (yrenard)
- - BoShurik
+ - Thomas Lallement (raziel057)
- Timothée Barray (tyx)
- James Halsall (jaitsu)
- - Hubert Lenoir (hubert_lenoir)
- - Florent Mata (fmata)
- Mikael Pajunen
- Warnar Boekkooi (boekkooi)
- Marco Petersen (ocrampete16)
@@ -303,13 +305,13 @@ The Symfony Connect username in parenthesis allows to get more information
- Dmitrii Chekaliuk (lazyhammer)
- Clément JOBEILI (dator)
- Vilius Grigaliūnas
- - Tom Van Looy (tvlooy)
- Marek Štípek (maryo)
- Patrick Landolt (scube)
- François Pluchino (francoispluchino)
- Daniel Espendiller
- Arnaud PETITPAS (apetitpa)
- Dorian Villet (gnutix)
+ - Wojciech Kania
- Alexey Kopytko (sanmai)
- Sergey Linnik (linniksa)
- Richard Miller
@@ -330,7 +332,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Julien Pauli
- Islam Israfilov (islam93)
- Oleg Andreyev (oleg.andreyev)
- - Thomas Lallement (raziel057)
- Daniel Gorgan
- Hendrik Luup (hluup)
- Martin Herndl (herndlm)
@@ -366,6 +367,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Dominique Bongiraud
- Hidde Wieringa (hiddewie)
- Christopher Davis (chrisguitarguy)
+ - Lukáš Holeczy (holicz)
- Florian Lonqueu-Brochard (florianlb)
- Leszek Prabucki (l3l0)
- Emanuele Panzeri (thepanz)
@@ -391,7 +393,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Pascal Montoya
- Julien Brochet
- Michaël Perrin (michael.perrin)
- - Wojciech Kania
- Tristan Darricau (tristandsensio)
- Fabien S (bafs)
- Victor Bocharsky (bocharsky_bw)
@@ -425,6 +426,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Iker Ibarguren (ikerib)
- Manuel Reinhard (sprain)
- Johann Pardanaud
+ - Alexis Lefebvre
- Indra Gunawan (indragunawan)
- Tim Goudriaan (codedmonkey)
- Harm van Tilborg (hvt)
@@ -449,7 +451,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Xavier Perez
- Arjen Brouwer (arjenjb)
- Tavo Nieves J (tavoniievez)
- - Lukáš Holeczy (holicz)
- Arjen van der Meijden
- Patrick McDougle (patrick-mcdougle)
- Jerzy (jlekowski)
@@ -467,14 +468,17 @@ The Symfony Connect username in parenthesis allows to get more information
- David Badura (davidbadura)
- Uwe Jäger (uwej711)
- Eugene Leonovich (rybakit)
+ - Damien Alexandre (damienalexandre)
- Joseph Rouff (rouffj)
- Félix Labrecque (woodspire)
- GordonsLondon
- Roman Anasal
- Jan Sorgalla (jsor)
- Piotr Kugla (piku235)
+ - Quynh Xuan Nguyen (seriquynh)
- Ray
- Philipp Cordes (corphi)
+ - Simon Podlipsky (simpod)
- Chekote
- bhavin (bhavin4u)
- Pavel Popov (metaer)
@@ -494,6 +498,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Frank de Jonge
- Chris Tanaskoski
- julien57
+ - Loïc Frémont (loic425)
- Ben Ramsey (ramsey)
- Matthieu Auger (matthieuauger)
- Josip Kruslin (jkruslin)
@@ -505,12 +510,12 @@ The Symfony Connect username in parenthesis allows to get more information
- Beau Simensen (simensen)
- Robert Kiss (kepten)
- Zan Baldwin (zanbaldwin)
- - Alexis Lefebvre
- Antonio J. García Lagar (ajgarlag)
- Alexandre Quercia (alquerci)
- Marcos Sánchez
- Jérôme Tanghe (deuchnord)
- Kim Hemsø Rasmussen (kimhemsoe)
+ - Maximilian Reichel (phramz)
- Dane Powell
- jaugustin
- Dmytro Borysovskyi (dmytr0)
@@ -530,7 +535,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Martin Kirilov (wucdbm)
- Chris Smith (cs278)
- Florian Klein (docteurklein)
- - Damien Alexandre (damienalexandre)
- Bilge
- Rhodri Pugh (rodnaph)
- Manuel Kiessling (manuelkiessling)
@@ -544,15 +548,14 @@ The Symfony Connect username in parenthesis allows to get more information
- Andrew Moore (finewolf)
- Bertrand Zuchuat (garfield-fr)
- Marc Morera (mmoreram)
- - Quynh Xuan Nguyen (seriquynh)
- Gabor Toth (tgabi333)
- realmfoo
- Thomas Tourlourat (armetiz)
- Andrey Esaulov (andremaha)
- - Simon Podlipsky (simpod)
- Grégoire Passault (gregwar)
- Jerzy Zawadzki (jzawadzki)
- Ismael Ambrosi (iambrosi)
+ - Yannick Ihmels (ihmels)
- Saif Eddin G
- Emmanuel BORGES (eborges78)
- Aurelijus Valeiša (aurelijus)
@@ -566,6 +569,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Adrian Rudnik (kreischweide)
- Pavel Batanov (scaytrase)
- Francesc Rosàs (frosas)
+ - Andrii Dembitskyi
- Bongiraud Dominique
- janschoenherr
- Marko Kaznovac (kaznovac)
@@ -577,7 +581,6 @@ The Symfony Connect username in parenthesis allows to get more information
- James Hemery
- Egor Taranov
- Philippe Segatori
- - Loïc Frémont (loic425)
- Adrian Nguyen (vuphuong87)
- benjaminmal
- Thierry T (lepiaf)
@@ -595,6 +598,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Erkhembayar Gantulga (erheme318)
- Fractal Zombie
- Gunnstein Lye (glye)
+ - Thomas Talbot (ioni)
- Kévin THERAGE (kevin_therage)
- Noémi Salaün (noemi-salaun)
- Michel Hunziker
@@ -638,6 +642,7 @@ The Symfony Connect username in parenthesis allows to get more information
- rtek
- Inal DJAFAR (inalgnu)
- Christian Gärtner (dagardner)
+ - Artem Stepin (astepin)
- Adrien Jourdier (eclairia)
- Ivan Grigoriev (greedyivan)
- Tomasz Kowalczyk (thunderer)
@@ -652,6 +657,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Thomas P
- Kristijan Kanalaš (kristijan_kanalas_infostud)
- Felix Labrecque
+ - mondrake (mondrake)
- Yaroslav Kiliba
- “Filip
- Simon Watiau (simonwatiau)
@@ -679,6 +685,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Dalibor Karlović
- Randy Geraads
- Sanpi (sanpi)
+ - James Gilliland (neclimdul)
- Eduardo Gulias (egulias)
- Andreas Leathley (iquito)
- Nathanael Noblet (gnat)
@@ -724,7 +731,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Tamas Szijarto
- stlrnz
- Adrien Wilmet (adrienfr)
- - Yannick Ihmels (ihmels)
- Alex Bacart
- hugovms
- Michele Locati
@@ -763,7 +769,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Shakhobiddin
- Kai
- Lee Rowlands
- - Maximilian Reichel (phramz)
- siganushka (siganushka)
- Alain Hippolyte (aloneh)
- Karoly Negyesi (chx)
@@ -793,7 +798,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Vadim Kharitonov (vadim)
- Oscar Cubo Medina (ocubom)
- Karel Souffriau
- - Andrii Dembitskyi
- Christophe L. (christophelau)
- Daniël Brekelmans (dbrekelmans)
- Simon Heimberg (simon_heimberg)
@@ -814,12 +818,14 @@ The Symfony Connect username in parenthesis allows to get more information
- Thiago Cordeiro (thiagocordeiro)
- Julien Maulny
- Brian King
+ - Paul Oms
- Steffen Roßkamp
- Alexandru Furculita (afurculita)
- Michel Salib (michelsalib)
- Valentin Jonovs
- geoffrey
- Bastien DURAND (deamon)
+ - Benoit Galati (benoitgalati)
- Jon Gotlin (jongotlin)
- Jeanmonod David (jeanmonod)
- Daniel González (daniel.gonzalez)
@@ -833,6 +839,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Markus Bachmann (baachi)
- Roger Guasch (rogerguasch)
- Luis Tacón (lutacon)
+ - Alex Hofbauer (alexhofbauer)
- Andrii Popov (andrii-popov)
- lancergr
- Ivan Nikolaev (destillat)
@@ -842,6 +849,7 @@ The Symfony Connect username in parenthesis allows to get more information
- ampaze
- Arturs Vonda
- Xavier Briand (xavierbriand)
+ - Daniel Badura
- Asmir Mustafic (goetas)
- vagrant
- Asier Illarramendi (doup)
@@ -851,7 +859,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Vlad Gregurco (vgregurco)
- Boris Vujicic (boris.vujicic)
- Chris Sedlmayr (catchamonkey)
- - mondrake (mondrake)
- Kamil Kokot (pamil)
- Seb Koelen
- Christoph Mewes (xrstf)
@@ -893,13 +900,14 @@ The Symfony Connect username in parenthesis allows to get more information
- Pablo Díez (pablodip)
- Damien Fa
- Kevin McBride
+ - BrokenSourceCode
- Sergio Santoro
- - James Gilliland (neclimdul)
- Philipp Rieber (bicpi)
- Dennis Væversted (srnzitcom)
- Manuel de Ruiter (manuel)
- nikos.sotiropoulos
- Eduardo Oliveira (entering)
+ - Jonathan Johnson (jrjohnson)
- Eugene Wissner
- Ricardo Oliveira (ricardolotr)
- Roy Van Ginneken (rvanginneken)
@@ -907,6 +915,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Barry vd. Heuvel (barryvdh)
- Jon Dufresne
- Chad Sikorra (chadsikorra)
+ - Mathias Brodala (mbrodala)
- Evan S Kaufman (evanskaufman)
- Jonathan Sui Lioung Lee Slew (jlslew)
- mcben
@@ -914,6 +923,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Filip Procházka (fprochazka)
- stoccc
- Markus Lanthaler (lanthaler)
+ - Gigino Chianese (sajito)
- Xav` (xavismeh)
- Remi Collet
- Mathieu Rochette (mathroc)
@@ -957,6 +967,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Rodrigo Borrego Bernabé (rodrigobb)
- John Bafford (jbafford)
- Emanuele Iannone
+ - Gasan Guseynov (gassan)
- Ondrej Machulda (ondram)
- Denis Gorbachev (starfall)
- Martin Morávek (keeo)
@@ -990,12 +1001,12 @@ The Symfony Connect username in parenthesis allows to get more information
- Markus S. (staabm)
- Geoffrey Tran (geoff)
- Elan Ruusamäe (glen)
+ - Brad Jones
- Nicolas de Marqué (nicola)
- a.dmitryuk
- Jannik Zschiesche
- Jan Ole Behrens (deegital)
- Mantas Var (mvar)
- - Paul Oms
- Yann LUCAS (drixs6o9)
- Sebastian Krebs
- Htun Htun Htet (ryanhhh91)
@@ -1036,7 +1047,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Iliya Miroslavov Iliev (i.miroslavov)
- Safonov Nikita (ns3777k)
- Simon DELICATA
+ - Thibault Buathier (gwemox)
- vitaliytv
+ - Arnaud Frézet
- Nicolas Martin (cocorambo)
- luffy1727
- LHommet Nicolas (nicolaslh)
@@ -1044,7 +1057,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Amirreza Shafaat (amirrezashafaat)
- Laurent Clouet
- Adoni Pavlakis (adoni)
- - Alex Hofbauer (alexhofbauer)
- Maarten Nusteling (nusje2000)
- Ahmed EBEN HASSINE (famas23)
- Eduard Bulava (nonanerz)
@@ -1096,6 +1108,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jacek Wilczyński (jacekwilczynski)
- Hany el-Kerdany
- Wang Jingyu
+ - Benjamin Georgeault (wedgesama)
- Åsmund Garfors
- Maxime Douailin
- Jean Pasdeloup
@@ -1105,6 +1118,7 @@ The Symfony Connect username in parenthesis allows to get more information
- tamar peled
- Reinier Kip
- Geoffrey Brier (geoffrey-brier)
+ - Sofien Naas
- Christophe Meneses (c77men)
- Vladimir Tsykun
- Andrei O
@@ -1167,6 +1181,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Israel J. Carberry
- Julius Kiekbusch
- Miquel Rodríguez Telep (mrtorrent)
+ - Tamás Nagy (t-bond)
- Sergey Kolodyazhnyy (skolodyazhnyy)
- umpirski
- Benjamin
@@ -1174,6 +1189,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Chris Heng (gigablah)
- Oleksii Svitiashchuk
- Tristan Bessoussa (sf_tristanb)
+ - FORT Pierre-Louis (plfort)
- Richard Bradley
- Nathanaël Martel (nathanaelmartel)
- Nicolas Jourdan (nicolasjc)
@@ -1189,7 +1205,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Andreas Erhard (andaris)
- Evgeny Efimov (edefimov)
- John VanDeWeghe
- - Daniel Badura
- Oleg Mifle
- gnito-org
- Michael Devery (mickadoo)
@@ -1198,6 +1213,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Markkus Millend
- Clément
- Jorrit Schippers (jorrit)
+ - Aurimas Niekis (aurimasniekis)
- maxime.perrimond
- rvoisin
- cthulhu
@@ -1286,6 +1302,7 @@ The Symfony Connect username in parenthesis allows to get more information
- develop
- flip111
- Artem Oliinyk (artemoliynyk)
+ - Marvin Feldmann (breyndotechse)
- fruty
- VJ
- RJ Garcia
@@ -1298,9 +1315,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Ondrej Exner
- Mark Sonnabaum
- Adiel Cristo (arcristo)
- - Artem Stepin (astepin)
- Fabian Kropfhamer (fabiank)
- Junaid Farooq (junaidfarooq)
+ - Chris Jones (magikid)
- Massimiliano Braglia (massimilianobraglia)
- Swen van Zanten (swenvanzanten)
- Frankie Wittevrongel
@@ -1346,6 +1363,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Tayfun Aydin
- Arne Groskurth
- Ilya Chekalsky
+ - zenas1210
- Ostrzyciel
- Julien DIDIER (juliendidier)
- Ilia Sergunin (maranqz)
@@ -1384,6 +1402,7 @@ The Symfony Connect username in parenthesis allows to get more information
- BrokenSourceCode
- Fabian Haase
- Nikita Popov (nikic)
+ - Robert Fischer (sandoba)
- Tarjei Huse (tarjei)
- Besnik Br
- Michael Olšavský
@@ -1475,6 +1494,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Martin (meckhardt)
- Radosław Kowalewski
- JustDylan23
+ - buffcode
- Juraj Surman
- Victor
- Andreas Allacher
@@ -1493,7 +1513,6 @@ The Symfony Connect username in parenthesis allows to get more information
- John Stevenson
- everyx
- Stanislav Gamayunov (happyproff)
- - Jonathan Johnson (jrjohnson)
- Alexander McCullagh (mccullagh)
- Paul L McNeely (mcneely)
- Mike Meier (mykon)
@@ -1504,6 +1523,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Francis Turmel (fturmel)
- Nikita Nefedov (nikita2206)
- Bernat Llibre
+ - Daniel Burger
- cgonzalez
- Ben
- Joni Halme
@@ -1534,6 +1554,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Zhuravlev Alexander (scif)
- Stefano Degenkamp (steef)
- James Michael DuPont
+ - kor3k kor3k (kor3k)
- Eric Schildkamp
- agaktr
- Vincent CHALAMON
@@ -1725,7 +1746,6 @@ The Symfony Connect username in parenthesis allows to get more information
- robmro27
- Vallel Blanco
- Bastien Clément
- - Thomas Talbot
- Benjamin Franzke
- Pavinthan
- Sylvain METAYER
@@ -1784,7 +1804,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Evgeniy Koval
- Claas Augner
- Balazs Csaba
- - Benoit Galati (benoitgalati)
- Bill Hance (billhance)
- Douglas Reith (douglas_reith)
- Harry Walter (haswalt)
@@ -1817,7 +1836,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Michel Bardelmeijer
- Ikko Ashimine
- Erwin Dirks
- - Brad Jones
- Markus Ramšak
- den
- George Dietrich
@@ -1889,7 +1907,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Jordane VASPARD (elementaire)
- Erwan Nader (ernadoo)
- Faizan Akram Dar (faizanakram)
- - Gasan Guseynov (gassan)
- Greg Szczotka (greg606)
- Ian Littman (iansltx)
- Nathan DIdier (icz)
@@ -1901,7 +1918,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Florent Viel (luxifer)
- Maks 3w (maks3w)
- Mamikon Arakelyan (mamikon)
- - Mathias Brodala (mbrodala)
- Michiel Boeckaert (milio)
- Mike Milano (mmilano)
- Guillaume Lajarige (molkobain)
@@ -1945,21 +1961,23 @@ The Symfony Connect username in parenthesis allows to get more information
- Antoine Bluchet (soyuka)
- Patrick Kaufmann
- Anton Dyshkant
+ - Kirill Nesmeyanov (serafim)
- Reece Fowell (reecefowell)
- Guillaume Gammelin
- Valérian Galliat
- d-ph
- Renan Taranto (renan-taranto)
- - Thomas Talbot
- Rikijs Murgs
- Uladzimir Tsykun
- Amaury Leroux de Lens (amo__)
- Christian Jul Jensen
+ - Franck RANAIVO-HARISOA (franckranaivo)
- Alexandre GESLIN
- The Whole Life to Learn
- Mikkel Paulson
- ergiegonzaga
- Liverbool (liverbool)
+ - Julien Boudry
- Dalibor Karlović
- Sam Malone
- Ha Phan (haphan)
@@ -1976,15 +1994,18 @@ The Symfony Connect username in parenthesis allows to get more information
- Ganesh Chandrasekaran (gxc4795)
- Sander Marechal
- Franz Wilding (killerpoke)
+ - Ferenczi Krisztian (fchris82)
- Oleg Golovakhin (doc_tr)
- Icode4Food (icode4food)
- Radosław Benkel
+ - Bert ter Heide (bertterheide)
- Kevin Nadin (kevinjhappy)
- jean pasqualini (darkilliant)
- Ross Motley (rossmotley)
- ttomor
- Mei Gwilym (meigwilym)
- Michael H. Arieli
+ - Jitendra Adhikari (adhocore)
- Tom Panier (neemzy)
- Fred Cox
- Luciano Mammino (loige)
@@ -1994,8 +2015,10 @@ The Symfony Connect username in parenthesis allows to get more information
- Anne-Sophie Bachelard
- Marvin Butkereit
- Ben Oman
+ - Jack Worman (jworman)
- Chris de Kok
- Andreas Kleemann (andesk)
+ - Hubert Moreau (hmoreau)
- Manuele Menozzi
- Anton Babenko (antonbabenko)
- Irmantas Šiupšinskas (irmantas)
@@ -2033,6 +2056,7 @@ The Symfony Connect username in parenthesis allows to get more information
- tamirvs
- gauss
- julien.galenski
+ - Florian Guimier
- Christian Neff (secondtruth)
- Chris Tiearney
- Oliver Hoff
@@ -2043,6 +2067,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Goran Juric
- Laurent G. (laurentg)
- Nicolas Macherey
+ - Bhujagendra Ishaya
- Guido Donnari
- Mert Simsek (mrtsmsk0)
- Lin Clark
@@ -2070,12 +2095,12 @@ The Symfony Connect username in parenthesis allows to get more information
- Paul Mitchum (paul-m)
- Angel Koilov (po_taka)
- Dan Finnie
- - Sofien Naas
- Ken Marfilla (marfillaster)
- Max Grigorian (maxakawizard)
- benatespina (benatespina)
- Denis Kop
- Jean-Guilhem Rouel (jean-gui)
+ - Ivan Yivoff
- EdgarPE
- jfcixmedia
- Dominic Tubach
@@ -2086,11 +2111,11 @@ The Symfony Connect username in parenthesis allows to get more information
- Serge (nfx)
- Mikkel Paulson
- Michał Strzelecki
- - Aurimas Niekis (aurimasniekis)
- Hugo Fonseca (fonsecas72)
- Martynas Narbutas
- Bailey Parker
- Antanas Arvasevicius
+ - Kris Kelly
- Eddie Abou-Jaoude (eddiejaoude)
- Haritz Iturbe (hizai)
- Nerijus Arlauskas (nercury)
@@ -2113,7 +2138,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Jonathan Hedstrom
- Peter Smeets (darkspartan)
- Julien Bianchi (jubianchi)
- - Tamás Nagy (t-bond)
- Robert Meijers
- Tijs Verkoyen
- James Sansbury
@@ -2177,6 +2201,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Oxan van Leeuwen
- pkowalczyk
- Soner Sayakci
+ - Andreas Hennings
- Max Voloshin (maxvoloshin)
- Nicolas Fabre (nfabre)
- Raul Rodriguez (raul782)
@@ -2184,11 +2209,11 @@ The Symfony Connect username in parenthesis allows to get more information
- MightyBranch
- Kacper Gunia (cakper)
- Derek Lambert (dlambert)
+ - Mark Pedron (markpedron)
- Peter Thompson (petert82)
- error56
- Felicitus
- alexpozzi
- - Marvin Feldmann (breyndotechse)
- Krzysztof Przybyszewski (kprzybyszewski)
- Boullé William (williamboulle)
- Frederic Godfrin
@@ -2226,7 +2251,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Jelte Steijaert (jelte)
- David Négrier (moufmouf)
- Quique Porta (quiqueporta)
+ - Tobias Feijten (tobias93)
- Andrea Quintino (dirk39)
+ - Andreas Heigl (heiglandreas)
- Tomasz Szymczyk (karion)
- Peter Dietrich (xosofox)
- Alex Vasilchenko
@@ -2249,7 +2276,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Ross Tuck
- omniError
- Zander Baldwin
+ - László GÖRÖG
- Kévin Gomez (kevin)
+ - Kevin van Sonsbeek (kevin_van_sonsbeek)
- Mihai Nica (redecs)
- Andrei Igna
- azine
@@ -2299,8 +2328,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Steve Frécinaux
- Constantine Shtompel
- Jules Lamur
- - zenas1210
- Renato Mendes Figueiredo
+ - Raphaël Droz
- Eric Stern
- ShiraNai7
- Antal Áron (antalaron)
@@ -2318,6 +2347,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jason Desrosiers
- m.chwedziak
- Andreas Frömer
+ - Bikal Basnet
- Philip Frank
- Lance McNearney
- Illia Antypenko (aivus)
@@ -2339,10 +2369,10 @@ The Symfony Connect username in parenthesis allows to get more information
- Martin Pärtel
- Frédéric Bouchery (fbouchery)
- Patrick Daley (padrig)
+ - Phillip Look (plook)
- Max Summe
- Ema Panz
- Chihiro Adachi (chihiro-adachi)
- - Benjamin Georgeault (wedgesama)
- Raphaëll Roussel
- Tadcka
- Abudarham Yuval
@@ -2400,6 +2430,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Michael Lively (mlivelyjr)
- Abderrahim (phydev)
- Attila Bukor (r1pp3rj4ck)
+ - Thomas Boileau (tboileau)
- Thomas Chmielowiec (chmielot)
- Jānis Lukss
- rkerner
@@ -2410,6 +2441,7 @@ The Symfony Connect username in parenthesis allows to get more information
- AnrDaemon
- Charly Terrier (charlypoppins)
- Emre Akinci (emre)
+ - Rustam Bakeev (nommyde)
- psampaz (psampaz)
- Maxwell Vandervelde
- kaywalker
@@ -2441,7 +2473,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Ciaran McNulty (ciaranmcnulty)
- Andrew (drew)
- j4nr6n (j4nr6n)
- - kor3k kor3k (kor3k)
- Stelian Mocanita (stelian)
- Gautier Deuette
- Kirk Madera
@@ -2469,6 +2500,7 @@ The Symfony Connect username in parenthesis allows to get more information
- georaldc
- wusuopu
- Wouter de Wild
+ - Peter Potrowl
- povilas
- Gavin Staniforth
- Alessandro Tagliapietra (alex88)
@@ -2495,6 +2527,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Martin Schophaus (m_schophaus_adcada)
- Martynas Sudintas (martiis)
- Anton Sukhachev (mrsuh)
+ - Marcel Siegert
- ryunosuke
- Francisco Facioni (fran6co)
- Iwan van Staveren (istaveren)
@@ -2513,12 +2546,14 @@ The Symfony Connect username in parenthesis allows to get more information
- Matt Farmer
- catch
- Alexandre Segura
+ - Asier Etxebeste
- Josef Cech
- Andrii Boiko
- Harold Iedema
- Ikhsan Agustian
- Benoit Lévêque (benoit_leveque)
- Simon Bouland (bouland)
+ - Jakub Janata (janatjak)
- Jibé Barth (jibbarth)
- Matthew Foster (mfoster)
- Reyo Stallenberg (reyostallenberg)
@@ -2552,6 +2587,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Houziaux mike
- Phobetor
- Markus
+ - Janusz Mocek
- Thomas Chmielowiec
- shdev
- Andrey Ryaguzov
@@ -2569,6 +2605,7 @@ The Symfony Connect username in parenthesis allows to get more information
- František Bereň
- Jeremiah VALERIE
- Mike Francis
+ - Nil Borodulia
- Almog Baku (almogbaku)
- Gerd Christian Kunze (derdu)
- Ionel Scutelnicu (ionelscutelnicu)
@@ -2578,6 +2615,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Nick Stemerdink
- David Stone
- Grayson Koonce
+ - Wissame MEKHILEF
- Romain Dorgueil
- Christopher Parotat
- Dennis Haarbrink
@@ -2598,6 +2636,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Felix Marezki
- Normunds
- Thomas Rothe
+ - Troy Crawford
- nietonfir
- alefranz
- David Barratt
@@ -2623,6 +2662,7 @@ The Symfony Connect username in parenthesis allows to get more information
- efeen
- Nicolas Pion
- Muhammed Akbulut
+ - Xesau
- Aaron Somi
- Michał Dąbrowski (defrag)
- Simone Fumagalli (hpatoio)
@@ -2644,10 +2684,12 @@ The Symfony Connect username in parenthesis allows to get more information
- Gijs Kunze
- Artyom Protaskin
- Nathanael d. Noblet
+ - Yurun
- helmer
- ged15
- Simon Asika
- Daan van Renterghem
+ - Boudry Julien
- amcastror
- Bram Van der Sype (brammm)
- Guile (guile)
@@ -2723,6 +2765,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Rémi Blaise
- Nicolas Séverin
- Joel Marcey
+ - zolikonta
- David Christmann
- root
- pf
@@ -2764,8 +2807,10 @@ The Symfony Connect username in parenthesis allows to get more information
- Jelle Kapitein
- Jochen Mandl
- Marin Nicolae
+ - Albert Prat
- Alessandro Loffredo
- Ian Phillips
+ - Remi Collet
- Haritz
- Matthieu Prat
- Brieuc Thomas
@@ -2791,6 +2836,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Erik van Wingerden
- Valouleloup
- Alexis MARQUIS
+ - Matheus Gontijo
- Gerrit Drost
- Linnaea Von Lavia
- Simon Mönch
@@ -2810,6 +2856,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Rafał
- Adria Lopez (adlpz)
- Aaron Scherer (aequasi)
+ - Alexandre Jardin (alexandre.jardin)
+ - Bart Brouwer (bartbrouwer)
- Rosio (ben-rosio)
- Simon Paarlberg (blamh)
- Masao Maeda (brtriver)
@@ -2836,6 +2884,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Kevin Verschaeve (keversc)
- Kevin Herrera (kherge)
- Luis Ramón López López (lrlopez)
+ - Matheo Daninos (mathdns)
- Mehdi Mabrouk (mehdidev)
- Bart Reunes (metalarend)
- Muriel (metalmumu)
@@ -2847,6 +2896,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Olivier Laviale (olvlvl)
- Pablo Monterde Perez (plebs)
- Jimmy Leger (redpanda)
+ - Mokhtar Tlili (sf-djuba)
- Marcin Szepczynski (szepczynski)
- Simone Di Maulo (toretto460)
- Cyrille Jouineau (tuxosaurus)
@@ -2861,6 +2911,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Taylan Kasap
- Michael Orlitzky
- Nicolas A. Bérard-Nault
+ - Francois Martin
- Saem Ghani
- Stefan Oderbolz
- Gabriel Moreira
@@ -2901,6 +2952,7 @@ The Symfony Connect username in parenthesis allows to get more information
- temperatur
- Paul Andrieux
- Cas
+ - Gwendolen Lynch
- ghazy ben ahmed
- Karolis
- Myke79
@@ -2941,6 +2993,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jack Wright
- MrNicodemuz
- Anonymous User
+ - demeritcowboy
- Paweł Tomulik
- Eric J. Duran
- Blackfelix
@@ -3025,7 +3078,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Yurii K
- Richard Trebichavský
- g123456789l
+ - Mark Ogilvie
- Jonathan Vollebregt
+ - Vladimir Vasilev
- oscartv
- DanSync
- Peter Zwosta
@@ -3053,6 +3108,7 @@ The Symfony Connect username in parenthesis allows to get more information
- sualko
- ADmad
- Nicolas Roudaire
+ - Abdouni Karim (abdounikarim)
- Andreas Forsblom (aforsblo)
- Alex Olmos (alexolmos)
- Cedric BERTOLINI (alsciende)
@@ -3150,6 +3206,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jesper Søndergaard Pedersen (zerrvox)
- Florent Cailhol
- szymek
+ - Konrad
- Kovacs Nicolas
- craigmarvelley
- Stano Turza
diff --git a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php
index 472330a859c6e..c4d3971edcaff 100644
--- a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php
+++ b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php
@@ -11,6 +11,7 @@
namespace Symfony\Bridge\Twig;
+use Composer\InstalledVersions;
use Symfony\Bundle\FullStack;
use Twig\Error\SyntaxError;
use Twig\TwigFilter;
@@ -98,6 +99,12 @@ private static function onUndefined(string $name, string $type, string $componen
return sprintf('Did you forget to %s? Unknown %s "%s".', self::FULL_STACK_ENABLE[$component], $type, $name);
}
- return sprintf('Did you forget to run "composer require symfony/%s"? Unknown %s "%s".', $component, $type, $name);
+ $missingPackage = 'symfony/'.$component;
+
+ if (class_exists(InstalledVersions::class) && InstalledVersions::isInstalled($missingPackage)) {
+ $missingPackage = 'symfony/twig-bundle';
+ }
+
+ return sprintf('Did you forget to run "composer require %s"? Unknown %s "%s".', $missingPackage, $type, $name);
}
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php
index 36c2f76e7e3cf..5e7dc36f206d8 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php
@@ -53,11 +53,6 @@ protected function configure()
Before running this command, the cache must be empty.
-This command does not generate the classes cache (as when executing this
-command, too many classes that should be part of the cache are already loaded
-in memory). Use curl or any other similar tool to warm up
-the classes cache if you want.
-
EOF
)
;
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 2313aea5d2d49..1335e0a80f7c2 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -113,6 +113,7 @@
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Middleware\RouterContextMiddleware;
+use Symfony\Component\Messenger\Stamp\SerializedMessageStamp;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
use Symfony\Component\Messenger\Transport\TransportInterface;
@@ -167,8 +168,10 @@
use Symfony\Component\Notifier\Bridge\Vonage\VonageTransportFactory;
use Symfony\Component\Notifier\Bridge\Yunpian\YunpianTransportFactory;
use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory;
+use Symfony\Component\Notifier\ChatterInterface;
use Symfony\Component\Notifier\Notifier;
use Symfony\Component\Notifier\Recipient\Recipient;
+use Symfony\Component\Notifier\TexterInterface;
use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
@@ -649,18 +652,30 @@ public function load(array $configs, ContainerBuilder $container)
$container->registerAttributeForAutoconfiguration(AsController::class, static function (ChildDefinition $definition, AsController $attribute): void {
$definition->addTag('controller.service_arguments');
});
- $container->registerAttributeForAutoconfiguration(AsMessageHandler::class, static function (ChildDefinition $definition, AsMessageHandler $attribute, \ReflectionClass|\ReflectionMethod $reflector): void {
- $tagAttributes = get_object_vars($attribute);
- $tagAttributes['from_transport'] = $tagAttributes['fromTransport'];
- unset($tagAttributes['fromTransport']);
- if ($reflector instanceof \ReflectionMethod) {
- if (isset($tagAttributes['method'])) {
- throw new LogicException(sprintf('AsMessageHandler attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name));
+
+ if (class_exists(SerializedMessageStamp::class)) {
+ // symfony/messenger >= 6.1
+ $container->registerAttributeForAutoconfiguration(AsMessageHandler::class, static function (ChildDefinition $definition, AsMessageHandler $attribute, \ReflectionClass|\ReflectionMethod $reflector): void {
+ $tagAttributes = get_object_vars($attribute);
+ $tagAttributes['from_transport'] = $tagAttributes['fromTransport'];
+ unset($tagAttributes['fromTransport']);
+ if ($reflector instanceof \ReflectionMethod) {
+ if (isset($tagAttributes['method'])) {
+ throw new LogicException(sprintf('AsMessageHandler attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name));
+ }
+ $tagAttributes['method'] = $reflector->getName();
}
- $tagAttributes['method'] = $reflector->getName();
- }
- $definition->addTag('messenger.message_handler', $tagAttributes);
- });
+ $definition->addTag('messenger.message_handler', $tagAttributes);
+ });
+ } else {
+ // symfony/messenger < 6.1
+ $container->registerAttributeForAutoconfiguration(AsMessageHandler::class, static function (ChildDefinition $definition, AsMessageHandler $attribute): void {
+ $tagAttributes = get_object_vars($attribute);
+ $tagAttributes['from_transport'] = $tagAttributes['fromTransport'];
+ unset($tagAttributes['fromTransport']);
+ $definition->addTag('messenger.message_handler', $tagAttributes);
+ });
+ }
if (!$container->getParameter('kernel.debug')) {
// remove tagged iterator argument for resource checkers
@@ -2482,11 +2497,13 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
$container->getDefinition('chatter.transports')->setArgument(0, $config['chatter_transports']);
} else {
$container->removeDefinition('chatter');
+ $container->removeAlias(ChatterInterface::class);
}
if ($config['texter_transports']) {
$container->getDefinition('texter.transports')->setArgument(0, $config['texter_transports']);
} else {
$container->removeDefinition('texter');
+ $container->removeAlias(TexterInterface::class);
}
if ($this->mailerConfigEnabled) {
diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
index 165797504bcb8..d189b88db5799 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
@@ -70,7 +70,7 @@ protected static function bootKernel(array $options = []): KernelInterface
$kernel = static::createKernel($options);
$kernel->boot();
- self::$kernel = $kernel;
+ static::$kernel = $kernel;
static::$booted = true;
return static::$kernel;
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
index a00b5b47bf674..9a480af2ac816 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
@@ -56,6 +56,8 @@
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
use Symfony\Component\Messenger\Transport\TransportFactory;
+use Symfony\Component\Notifier\ChatterInterface;
+use Symfony\Component\Notifier\TexterInterface;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
@@ -2018,7 +2020,9 @@ public function testNotifierWithoutTransports()
$this->assertTrue($container->hasDefinition('notifier'));
$this->assertFalse($container->hasDefinition('chatter'));
+ $this->assertFalse($container->hasAlias(ChatterInterface::class));
$this->assertFalse($container->hasDefinition('texter'));
+ $this->assertFalse($container->hasAlias(TexterInterface::class));
}
public function testIfNotifierTransportsAreKnownByFrameworkExtension()
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index 1a301287d043d..dc1e717e28595 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -48,7 +48,7 @@
"symfony/http-client": "^5.4|^6.0",
"symfony/lock": "^5.4|^6.0",
"symfony/mailer": "^5.4|^6.0",
- "symfony/messenger": "^6.1",
+ "symfony/messenger": "^5.4|^6.0",
"symfony/mime": "^5.4|^6.0",
"symfony/notifier": "^5.4|^6.0",
"symfony/process": "^5.4|^6.0",
diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php
index 64068fcc23b48..c88bf2c842105 100644
--- a/src/Symfony/Component/Console/Application.php
+++ b/src/Symfony/Component/Console/Application.php
@@ -957,22 +957,30 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
}
}
- if ($command instanceof SignalableCommandInterface && ($this->signalsToDispatchEvent || $command->getSubscribedSignals())) {
- if (!$this->signalRegistry) {
- throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
- }
+ if ($this->signalsToDispatchEvent) {
+ $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
- if (Terminal::hasSttyAvailable()) {
- $sttyMode = shell_exec('stty -g');
+ if ($commandSignals || null !== $this->dispatcher) {
+ if (!$this->signalRegistry) {
+ throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
+ }
- foreach ([\SIGINT, \SIGTERM] as $signal) {
- $this->signalRegistry->register($signal, static function () use ($sttyMode) {
- shell_exec('stty '.$sttyMode);
- });
+ if (Terminal::hasSttyAvailable()) {
+ $sttyMode = shell_exec('stty -g');
+
+ foreach ([\SIGINT, \SIGTERM] as $signal) {
+ $this->signalRegistry->register($signal, static function () use ($sttyMode) {
+ shell_exec('stty '.$sttyMode);
+ });
+ }
+ }
+
+ foreach ($commandSignals as $signal) {
+ $this->signalRegistry->register($signal, [$command, 'handleSignal']);
}
}
- if ($this->dispatcher) {
+ if (null !== $this->dispatcher) {
foreach ($this->signalsToDispatchEvent as $signal) {
$event = new ConsoleSignalEvent($command, $input, $output, $signal);
@@ -988,10 +996,6 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
});
}
}
-
- foreach ($command->getSubscribedSignals() as $signal) {
- $this->signalRegistry->register($signal, [$command, 'handleSignal']);
- }
}
if (null === $this->dispatcher) {
diff --git a/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php b/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php
index 5f32173ae5a48..2158339ec4181 100644
--- a/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php
+++ b/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php
@@ -127,7 +127,7 @@ private function sortCommands(array $commands): array
}
if ($namespacedCommands) {
- ksort($namespacedCommands);
+ ksort($namespacedCommands, \SORT_STRING);
foreach ($namespacedCommands as $key => $commandsSet) {
ksort($commandsSet);
$sortedCommands[$key] = $commandsSet;
diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php b/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php
index e72b641bae6f9..ee541dcd7765c 100644
--- a/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php
+++ b/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php
@@ -77,7 +77,7 @@ public function pop(OutputFormatterStyleInterface $style = null): OutputFormatte
/**
* Computes current style with stacks top codes.
*/
- public function getCurrent(): OutputFormatterStyle
+ public function getCurrent(): OutputFormatterStyleInterface
{
if (empty($this->styles)) {
return $this->emptyStyle;
diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php
index 9d6c619ad82ff..6295de3ecaf18 100644
--- a/src/Symfony/Component/Console/Tests/ApplicationTest.php
+++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php
@@ -22,6 +22,7 @@
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
+use Symfony\Component\Console\Event\ConsoleSignalEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
@@ -43,6 +44,8 @@
use Symfony\Component\Console\Tester\ApplicationTester;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Process\Process;
class ApplicationTest extends TestCase
@@ -1862,9 +1865,9 @@ public function testCommandNameMismatchWithCommandLoaderKeyThrows()
/**
* @requires extension pcntl
*/
- public function testSignal()
+ public function testSignalListenerNotCalledByDefault()
{
- $command = new SignableCommand();
+ $command = new SignableCommand(false);
$dispatcherCalled = false;
$dispatcher = new EventDispatcher();
@@ -1872,29 +1875,97 @@ public function testSignal()
$dispatcherCalled = true;
});
- $application = new Application();
- $application->setAutoExit(false);
- $application->setDispatcher($dispatcher);
- $application->setSignalsToDispatchEvent(\SIGALRM);
- $application->add(new LazyCommand('signal', [], '', false, function () use ($command) { return $command; }, true));
-
- $this->assertFalse($command->signaled);
- $this->assertFalse($dispatcherCalled);
+ $application = $this->createSignalableApplication($command, $dispatcher);
$this->assertSame(0, $application->run(new ArrayInput(['signal'])));
$this->assertFalse($command->signaled);
$this->assertFalse($dispatcherCalled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testSignalListener()
+ {
+ $command = new SignableCommand();
+
+ $dispatcherCalled = false;
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addListener('console.signal', function () use (&$dispatcherCalled) {
+ $dispatcherCalled = true;
+ });
+
+ $application = $this->createSignalableApplication($command, $dispatcher);
- $command->loop = 100000;
- pcntl_alarm(1);
$this->assertSame(1, $application->run(new ArrayInput(['signal'])));
- $this->assertTrue($command->signaled);
$this->assertTrue($dispatcherCalled);
+ $this->assertTrue($command->signaled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testSignalSubscriberNotCalledByDefault()
+ {
+ $command = new BaseSignableCommand(false);
+
+ $subscriber = new SignalEventSubscriber();
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addSubscriber($subscriber);
+
+ $application = $this->createSignalableApplication($command, $dispatcher);
+
+ $this->assertSame(0, $application->run(new ArrayInput(['signal'])));
+ $this->assertFalse($subscriber->signaled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testSignalSubscriber()
+ {
+ $command = new BaseSignableCommand();
+
+ $subscriber1 = new SignalEventSubscriber();
+ $subscriber2 = new SignalEventSubscriber();
+
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addSubscriber($subscriber1);
+ $dispatcher->addSubscriber($subscriber2);
+
+ $application = $this->createSignalableApplication($command, $dispatcher);
+
+ $this->assertSame(1, $application->run(new ArrayInput(['signal'])));
+ $this->assertTrue($subscriber1->signaled);
+ $this->assertTrue($subscriber2->signaled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testSetSignalsToDispatchEvent()
+ {
+ $command = new BaseSignableCommand();
+
+ $subscriber = new SignalEventSubscriber();
+
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addSubscriber($subscriber);
+
+ $application = $this->createSignalableApplication($command, $dispatcher);
+ $application->setSignalsToDispatchEvent(\SIGUSR2);
+ $this->assertSame(0, $application->run(new ArrayInput(['signal'])));
+ $this->assertFalse($subscriber->signaled);
+
+ $application = $this->createSignalableApplication($command, $dispatcher);
+ $application->setSignalsToDispatchEvent(\SIGUSR1);
+ $this->assertSame(1, $application->run(new ArrayInput(['signal'])));
+ $this->assertTrue($subscriber->signaled);
}
public function testSignalableCommandInterfaceWithoutSignals()
{
- $command = new SignableCommand();
+ $command = new SignableCommand(false);
$dispatcher = new EventDispatcher();
$application = new Application();
@@ -1936,6 +2007,18 @@ public function testSignalableRestoresStty()
$this->assertSame($previousSttyMode, $sttyMode);
}
+
+ private function createSignalableApplication(Command $command, ?EventDispatcherInterface $dispatcher): Application
+ {
+ $application = new Application();
+ $application->setAutoExit(false);
+ if ($dispatcher) {
+ $application->setDispatcher($dispatcher);
+ }
+ $application->add(new LazyCommand('signal', [], '', false, function () use ($command) { return $command; }, true));
+
+ return $application;
+ }
}
class CustomApplication extends Application
@@ -1988,23 +2071,24 @@ public function isEnabled(): bool
}
#[AsCommand(name: 'signal')]
-class SignableCommand extends Command implements SignalableCommandInterface
+class BaseSignableCommand extends Command
{
public $signaled = false;
- public $loop = 100;
-
- public function getSubscribedSignals(): array
- {
- return SignalRegistry::isSupported() ? [\SIGALRM] : [];
- }
+ public $loop = 1000;
+ private $emitsSignal;
- public function handleSignal(int $signal): void
+ public function __construct(bool $emitsSignal = true)
{
- $this->signaled = true;
+ parent::__construct();
+ $this->emitsSignal = $emitsSignal;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
+ if ($this->emitsSignal) {
+ posix_kill(posix_getpid(), SIGUSR1);
+ }
+
for ($i = 0; $i < $this->loop; ++$i) {
usleep(100);
if ($this->signaled) {
@@ -2015,3 +2099,33 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 0;
}
}
+
+#[AsCommand(name: 'signal')]
+class SignableCommand extends BaseSignableCommand implements SignalableCommandInterface
+{
+ public function getSubscribedSignals(): array
+ {
+ return SignalRegistry::isSupported() ? [\SIGUSR1] : [];
+ }
+
+ public function handleSignal(int $signal): void
+ {
+ $this->signaled = true;
+ }
+}
+
+class SignalEventSubscriber implements EventSubscriberInterface
+{
+ public $signaled = false;
+
+ public function onSignal(ConsoleSignalEvent $event): void
+ {
+ $this->signaled = true;
+ $event->getCommand()->signaled = true;
+ }
+
+ public static function getSubscribedEvents(): array
+ {
+ return ['console.signal' => 'onSignal'];
+ }
+}
diff --git a/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php b/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php
index b3ba9d8482b0f..da64dca00b949 100644
--- a/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php
+++ b/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php
@@ -36,7 +36,7 @@ public function getNamespacesProvider()
return [
[['_global'], ['foobar']],
[['a', 'b'], ['b:foo', 'a:foo', 'b:bar']],
- [['_global', 'b', 'z', 22, 33], ['z:foo', '1', '33:foo', 'b:foo', '22:foo:bar']],
+ [['_global', 22, 33, 'b', 'z'], ['z:foo', '1', '33:foo', 'b:foo', '22:foo:bar']],
];
}
}
diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php
index a6f850319897b..174095e13c8ef 100644
--- a/src/Symfony/Component/DomCrawler/Crawler.php
+++ b/src/Symfony/Component/DomCrawler/Crawler.php
@@ -555,7 +555,7 @@ public function text(string $default = null, bool $normalizeWhitespace = true):
$text = $this->getNode(0)->nodeValue;
if ($normalizeWhitespace) {
- return trim(preg_replace('/(?:\s{2,}+|[^\S ])/', ' ', $text));
+ return trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $text), " \n\r\t\x0C");
}
return $text;
diff --git a/src/Symfony/Component/Filesystem/Path.php b/src/Symfony/Component/Filesystem/Path.php
index 17e73a018a623..37dce5c01bc34 100644
--- a/src/Symfony/Component/Filesystem/Path.php
+++ b/src/Symfony/Component/Filesystem/Path.php
@@ -81,7 +81,7 @@ public static function canonicalize(string $path): string
// Replace "~" with user's home directory.
if ('~' === $path[0]) {
- $path = self::getHomeDirectory().mb_substr($path, 1);
+ $path = self::getHomeDirectory().substr($path, 1);
}
$path = self::normalize($path);
@@ -151,14 +151,14 @@ public static function getDirectory(string $path): string
$path = self::canonicalize($path);
// Maintain scheme
- if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) {
- $scheme = mb_substr($path, 0, $schemeSeparatorPosition + 3);
- $path = mb_substr($path, $schemeSeparatorPosition + 3);
+ if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
+ $scheme = substr($path, 0, $schemeSeparatorPosition + 3);
+ $path = substr($path, $schemeSeparatorPosition + 3);
} else {
$scheme = '';
}
- if (false === ($dirSeparatorPosition = strrpos($path, '/'))) {
+ if (false === $dirSeparatorPosition = strrpos($path, '/')) {
return '';
}
@@ -169,10 +169,10 @@ public static function getDirectory(string $path): string
// Directory equals Windows root "C:/"
if (2 === $dirSeparatorPosition && ctype_alpha($path[0]) && ':' === $path[1]) {
- return $scheme.mb_substr($path, 0, 3);
+ return $scheme.substr($path, 0, 3);
}
- return $scheme.mb_substr($path, 0, $dirSeparatorPosition);
+ return $scheme.substr($path, 0, $dirSeparatorPosition);
}
/**
@@ -219,7 +219,7 @@ public static function getRoot(string $path): string
}
// Maintain scheme
- if (false !== ($schemeSeparatorPosition = strpos($path, '://'))) {
+ if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
$scheme = substr($path, 0, $schemeSeparatorPosition + 3);
$path = substr($path, $schemeSeparatorPosition + 3);
} else {
@@ -233,7 +233,7 @@ public static function getRoot(string $path): string
return $scheme.'/';
}
- $length = mb_strlen($path);
+ $length = \strlen($path);
// Windows root
if ($length > 1 && ':' === $path[1] && ctype_alpha($firstCharacter)) {
@@ -349,16 +349,16 @@ public static function changeExtension(string $path, string $extension): string
$extension = ltrim($extension, '.');
// No extension for paths
- if ('/' === mb_substr($path, -1)) {
+ if ('/' === substr($path, -1)) {
return $path;
}
// No actual extension in path
if (empty($actualExtension)) {
- return $path.('.' === mb_substr($path, -1) ? '' : '.').$extension;
+ return $path.('.' === substr($path, -1) ? '' : '.').$extension;
}
- return mb_substr($path, 0, -mb_strlen($actualExtension)).$extension;
+ return substr($path, 0, -\strlen($actualExtension)).$extension;
}
public static function isAbsolute(string $path): bool
@@ -368,8 +368,8 @@ public static function isAbsolute(string $path): bool
}
// Strip scheme
- if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) {
- $path = mb_substr($path, $schemeSeparatorPosition + 3);
+ if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
+ $path = substr($path, $schemeSeparatorPosition + 3);
}
$firstCharacter = $path[0];
@@ -380,9 +380,9 @@ public static function isAbsolute(string $path): bool
}
// Windows root
- if (mb_strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) {
+ if (\strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) {
// Special case: "C:"
- if (2 === mb_strlen($path)) {
+ if (2 === \strlen($path)) {
return true;
}
@@ -451,9 +451,9 @@ public static function makeAbsolute(string $path, string $basePath): string
return self::canonicalize($path);
}
- if (false !== ($schemeSeparatorPosition = mb_strpos($basePath, '://'))) {
- $scheme = mb_substr($basePath, 0, $schemeSeparatorPosition + 3);
- $basePath = mb_substr($basePath, $schemeSeparatorPosition + 3);
+ if (false !== $schemeSeparatorPosition = strpos($basePath, '://')) {
+ $scheme = substr($basePath, 0, $schemeSeparatorPosition + 3);
+ $basePath = substr($basePath, $schemeSeparatorPosition + 3);
} else {
$scheme = '';
}
@@ -671,7 +671,7 @@ public static function join(string ...$paths): string
}
// Only add slash if previous part didn't end with '/' or '\'
- if (!\in_array(mb_substr($finalPath, -1), ['/', '\\'])) {
+ if (!\in_array(substr($finalPath, -1), ['/', '\\'])) {
$finalPath .= '/';
}
@@ -776,19 +776,19 @@ private static function split(string $path): array
}
// Remember scheme as part of the root, if any
- if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) {
- $root = mb_substr($path, 0, $schemeSeparatorPosition + 3);
- $path = mb_substr($path, $schemeSeparatorPosition + 3);
+ if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
+ $root = substr($path, 0, $schemeSeparatorPosition + 3);
+ $path = substr($path, $schemeSeparatorPosition + 3);
} else {
$root = '';
}
- $length = mb_strlen($path);
+ $length = \strlen($path);
// Remove and remember root directory
if (str_starts_with($path, '/')) {
$root .= '/';
- $path = $length > 1 ? mb_substr($path, 1) : '';
+ $path = $length > 1 ? substr($path, 1) : '';
} elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
if (2 === $length) {
// Windows special case: "C:"
@@ -796,8 +796,8 @@ private static function split(string $path): array
$path = '';
} elseif ('/' === $path[2]) {
// Windows normal case: "C:/"..
- $root .= mb_substr($path, 0, 3);
- $path = $length > 3 ? mb_substr($path, 3) : '';
+ $root .= substr($path, 0, 3);
+ $path = $length > 3 ? substr($path, 3) : '';
}
}
@@ -806,11 +806,11 @@ private static function split(string $path): array
private static function toLower(string $string): string
{
- if (false !== $encoding = mb_detect_encoding($string)) {
+ if (false !== $encoding = mb_detect_encoding($string, null, true)) {
return mb_strtolower($string, $encoding);
}
- return strtolower($string, $encoding);
+ return strtolower($string);
}
private function __construct()
diff --git a/src/Symfony/Component/Filesystem/Tests/PathTest.php b/src/Symfony/Component/Filesystem/Tests/PathTest.php
index 4fb2c013066f9..2f04c790c396a 100644
--- a/src/Symfony/Component/Filesystem/Tests/PathTest.php
+++ b/src/Symfony/Component/Filesystem/Tests/PathTest.php
@@ -223,6 +223,8 @@ public function provideGetDirectoryTests(): \Generator
yield ['/..', '/'];
yield ['C:webmozart', ''];
+
+ yield ['D:/Folder/Aééé/Subfolder', 'D:/Folder/Aééé'];
}
/**
diff --git a/src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php b/src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php
index 9e1e0af5202de..57785a89679f2 100644
--- a/src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php
+++ b/src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php
@@ -15,6 +15,7 @@
use Symfony\Component\Form\Exception\AccessException;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\PropertyAccess\Exception\AccessException as PropertyAccessException;
+use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
@@ -90,6 +91,10 @@ private function getPropertyValue(object|array $data, PropertyPathInterface $pro
try {
return $this->propertyAccessor->getValue($data, $propertyPath);
} catch (PropertyAccessException $e) {
+ if (\is_array($data) && $e instanceof NoSuchIndexException) {
+ return null;
+ }
+
if (!$e instanceof UninitializedPropertyException
// For versions without UninitializedPropertyException check the exception message
&& (class_exists(UninitializedPropertyException::class) || !str_contains($e->getMessage(), 'You should initialize it'))
diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php
index 5244948a0cc17..e5a4aeec332aa 100644
--- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php
+++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php
@@ -14,7 +14,9 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Exception\AlreadySubmittedException;
+use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
+use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
@@ -36,6 +38,7 @@
use Symfony\Component\Form\Tests\Fixtures\Map;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\PropertyAccess\PropertyAccess;
class CompoundFormTest extends TestCase
{
@@ -1076,6 +1079,30 @@ public function testFileUpload()
$this->assertNull($this->form->get('bar')->getData());
}
+ public function testMapDateTimeObjectsWithEmptyArrayDataUsingDataMapper()
+ {
+ $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
+ ->enableExceptionOnInvalidIndex()
+ ->getPropertyAccessor();
+ $form = $this->factory->createBuilder()
+ ->setDataMapper(new DataMapper(new PropertyPathAccessor($propertyAccessor)))
+ ->add('date', DateType::class, [
+ 'auto_initialize' => false,
+ 'format' => 'dd/MM/yyyy',
+ 'html5' => false,
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'single_text',
+ ])
+ ->getForm();
+
+ $form->submit([
+ 'date' => '04/08/2022',
+ ]);
+
+ $this->assertEquals(['date' => new \DateTime('2022-08-04', new \DateTimeZone('UTC'))], $form->getData());
+ }
+
private function createForm(string $name = 'name', bool $compound = true): FormInterface
{
$builder = $this->getBuilder($name);
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/DataMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/DataMapperTest.php
index b29cb3b5e8980..1b625dc139adc 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/DataMapperTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/DataMapperTest.php
@@ -14,10 +14,14 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
+use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormConfigBuilder;
+use Symfony\Component\Form\FormFactoryBuilder;
use Symfony\Component\Form\Tests\Fixtures\TypehintedPropertiesCar;
+use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyPath;
class DataMapperTest extends TestCase
@@ -382,6 +386,33 @@ public function testMapFormsToDataUsingSetCallbackOption()
self::assertSame('Jane Doe', $person->myName());
}
+
+ public function testMapFormsToDataMapsDateTimeInstanceToArrayIfNotSetBefore()
+ {
+ $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
+ ->enableExceptionOnInvalidIndex()
+ ->getPropertyAccessor();
+ $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
+ ->enableExceptionOnInvalidIndex()
+ ->getPropertyAccessor();
+ $form = (new FormFactoryBuilder())->getFormFactory()->createBuilder()
+ ->setDataMapper(new DataMapper(new PropertyPathAccessor($propertyAccessor)))
+ ->add('date', DateType::class, [
+ 'auto_initialize' => false,
+ 'format' => 'dd/MM/yyyy',
+ 'html5' => false,
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'single_text',
+ ])
+ ->getForm();
+
+ $form->submit([
+ 'date' => '04/08/2022',
+ ]);
+
+ $this->assertEquals(['date' => new \DateTime('2022-08-04', new \DateTimeZone('UTC'))], $form->getData());
+ }
}
class SubmittedForm extends Form
diff --git a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
index 1634243ade40f..fa7120f0a1eeb 100644
--- a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
+++ b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
@@ -96,7 +96,7 @@ public function reset()
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS);
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION);
- if (\defined('CURL_LOCK_DATA_CONNECT')) {
+ if (\defined('CURL_LOCK_DATA_CONNECT') && \PHP_VERSION_ID >= 80000) {
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT);
}
}
diff --git a/src/Symfony/Component/HttpClient/Response/StreamWrapper.php b/src/Symfony/Component/HttpClient/Response/StreamWrapper.php
index e002a004816d6..749be2688d4d8 100644
--- a/src/Symfony/Component/HttpClient/Response/StreamWrapper.php
+++ b/src/Symfony/Component/HttpClient/Response/StreamWrapper.php
@@ -59,20 +59,18 @@ public static function createResource(ResponseInterface $response, HttpClientInt
throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__));
}
- if (false === stream_wrapper_register('symfony', __CLASS__)) {
+ static $registered = false;
+
+ if (!$registered = $registered || stream_wrapper_register(strtr(__CLASS__, '\\', '-'), __CLASS__)) {
throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.');
}
- try {
- $context = [
- 'client' => $client ?? $response,
- 'response' => $response,
- ];
-
- return fopen('symfony://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context])) ?: null;
- } finally {
- stream_wrapper_unregister('symfony');
- }
+ $context = [
+ 'client' => $client ?? $response,
+ 'response' => $response,
+ ];
+
+ return fopen(strtr(__CLASS__, '\\', '-').'://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context]));
}
public function getResponse(): ResponseInterface
diff --git a/src/Symfony/Component/HttpFoundation/RateLimiter/AbstractRequestRateLimiter.php b/src/Symfony/Component/HttpFoundation/RateLimiter/AbstractRequestRateLimiter.php
index c91d614fe30bf..a6dd993b7315b 100644
--- a/src/Symfony/Component/HttpFoundation/RateLimiter/AbstractRequestRateLimiter.php
+++ b/src/Symfony/Component/HttpFoundation/RateLimiter/AbstractRequestRateLimiter.php
@@ -35,9 +35,7 @@ public function consume(Request $request): RateLimit
foreach ($limiters as $limiter) {
$rateLimit = $limiter->consume(1);
- if (null === $minimalRateLimit || $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()) {
- $minimalRateLimit = $rateLimit;
- }
+ $minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit;
}
return $minimalRateLimit;
@@ -54,4 +52,20 @@ public function reset(Request $request): void
* @return LimiterInterface[] a set of limiters using keys extracted from the request
*/
abstract protected function getLimiters(Request $request): array;
+
+ private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit
+ {
+ if ($first->isAccepted() !== $second->isAccepted()) {
+ return $first->isAccepted() ? $second : $first;
+ }
+
+ $firstRemainingTokens = $first->getRemainingTokens();
+ $secondRemainingTokens = $second->getRemainingTokens();
+
+ if ($firstRemainingTokens === $secondRemainingTokens) {
+ return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first;
+ }
+
+ return $firstRemainingTokens > $secondRemainingTokens ? $second : $first;
+ }
}
diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php
index 730f1009138ab..f507d926935ae 100644
--- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php
+++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php
@@ -30,6 +30,16 @@ public function __construct(\SessionHandlerInterface $handler)
$this->handler = $handler;
}
+ /**
+ * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler.
+ *
+ * @internal
+ */
+ public function isWrapper(): bool
+ {
+ return $this->handler instanceof \SessionHandler;
+ }
+
public function open(string $savePath, string $sessionName): bool
{
parent::open($savePath, $sessionName);
diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php
index 73376619ce73e..c292e58f05851 100644
--- a/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php
+++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy;
+use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
+
/**
* @author Drak
*/
@@ -22,7 +24,7 @@ public function __construct(\SessionHandlerInterface $handler)
{
$this->handler = $handler;
$this->wrapper = $handler instanceof \SessionHandler;
- $this->saveHandlerName = $this->wrapper ? \ini_get('session.save_handler') : 'user';
+ $this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user';
}
public function getHandler(): \SessionHandlerInterface
diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected
new file mode 100644
index 0000000000000..0afe8a6333d00
--- /dev/null
+++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected
@@ -0,0 +1,11 @@
+
+Array
+(
+ [0] => Content-Type: text/plain; charset=utf-8
+ [1] => Cache-Control: max-age=0, private, must-revalidate
+ [2] => Cache-Control: max-age=0, must-revalidate, private
+ [3] => Date: Sat, 12 Nov 1955 20:04:00 GMT
+ [4] => Expires: %s, %d %s %d %d:%d:%d GMT
+ [5] => Set-Cookie: PHPSESSID=deleted; expires=%s, %d %s %d %d:%d:%d GMT; Max-Age=%d; %s
+)
+shutdown
diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php
new file mode 100644
index 0000000000000..003b0c121f888
--- /dev/null
+++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php
@@ -0,0 +1,60 @@
+cookies->set($sessionName, $sessionId);
+
+$requestStack = new RequestStack();
+$requestStack->push($request);
+
+$sessionFactory = new SessionFactory($requestStack, new NativeSessionStorageFactory());
+
+$container = new Container();
+$container->set('request_stack', $requestStack);
+$container->set('session_factory', $sessionFactory);
+
+$listener = new SessionListener($container);
+
+$kernel = new class($r) implements HttpKernelInterface {
+ /**
+ * @var Response
+ */
+ private $response;
+
+ public function __construct(Response $response)
+ {
+ $this->response = $response;
+ }
+
+ public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response
+ {
+ return $this->response;
+ }
+};
+
+$listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST));
+$session = $request->getSession();
+$session->set('foo', 'bar');
+$session->invalidate();
+
+$listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $r));
+
+$r->sendHeaders();
diff --git a/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/AbstractRequestRateLimiterTest.php b/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/AbstractRequestRateLimiterTest.php
new file mode 100644
index 0000000000000..4790eae183802
--- /dev/null
+++ b/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/AbstractRequestRateLimiterTest.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Tests\RateLimiter;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\RateLimiter\LimiterInterface;
+use Symfony\Component\RateLimiter\RateLimit;
+
+class AbstractRequestRateLimiterTest extends TestCase
+{
+ /**
+ * @dataProvider provideRateLimits
+ */
+ public function testConsume(array $rateLimits, ?RateLimit $expected)
+ {
+ $rateLimiter = new MockAbstractRequestRateLimiter(array_map(function (RateLimit $rateLimit) {
+ $limiter = $this->createStub(LimiterInterface::class);
+ $limiter->method('consume')->willReturn($rateLimit);
+
+ return $limiter;
+ }, $rateLimits));
+
+ $this->assertSame($expected, $rateLimiter->consume(new Request()));
+ }
+
+ public function provideRateLimits()
+ {
+ $now = new \DateTimeImmutable();
+
+ yield 'Both accepted with different count of remaining tokens' => [
+ [
+ $expected = new RateLimit(0, $now, true, 1), // less remaining tokens
+ new RateLimit(1, $now, true, 1),
+ ],
+ $expected,
+ ];
+
+ yield 'Both accepted with same count of remaining tokens' => [
+ [
+ $expected = new RateLimit(0, $now->add(new \DateInterval('P1D')), true, 1), // longest wait time
+ new RateLimit(0, $now, true, 1),
+ ],
+ $expected,
+ ];
+
+ yield 'Accepted and denied' => [
+ [
+ new RateLimit(0, $now, true, 1),
+ $expected = new RateLimit(0, $now, false, 1), // denied
+ ],
+ $expected,
+ ];
+ }
+}
diff --git a/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/MockAbstractRequestRateLimiter.php b/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/MockAbstractRequestRateLimiter.php
new file mode 100644
index 0000000000000..0acc918bf4d5c
--- /dev/null
+++ b/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/MockAbstractRequestRateLimiter.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Tests\RateLimiter;
+
+use Symfony\Component\HttpFoundation\RateLimiter\AbstractRequestRateLimiter;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\RateLimiter\LimiterInterface;
+
+class MockAbstractRequestRateLimiter extends AbstractRequestRateLimiter
+{
+ /**
+ * @var LimiterInterface[]
+ */
+ private $limiters;
+
+ public function __construct(array $limiters)
+ {
+ $this->limiters = $limiters;
+ }
+
+ protected function getLimiters(Request $request): array
+ {
+ return $this->limiters;
+ }
+}
diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseFunctionalTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseFunctionalTest.php
index 5d1e81dcfc3aa..aca283af0023d 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/ResponseFunctionalTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseFunctionalTest.php
@@ -44,6 +44,7 @@ public static function tearDownAfterClass(): void
public function testCookie($fixture)
{
$result = file_get_contents(sprintf('http://localhost:8054/%s.php', $fixture));
+ $result = preg_replace_callback('/expires=[^;]++/', function ($m) { return str_replace('-', ' ', $m[0]); }, $result);
$this->assertStringMatchesFormatFile(__DIR__.sprintf('/Fixtures/response-functional/%s.expected', $fixture), $result);
}
diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php
index f6417720d27aa..aca2bfd882b20 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php
@@ -46,6 +46,7 @@ public function testSession($fixture)
$context = ['http' => ['header' => "Cookie: sid=123abc\r\n"]];
$context = stream_context_create($context);
$result = file_get_contents(sprintf('http://localhost:8053/%s.php', $fixture), false, $context);
+ $result = preg_replace_callback('/expires=[^;]++/', function ($m) { return str_replace('-', ' ', $m[0]); }, $result);
$this->assertStringEqualsFile(__DIR__.sprintf('/Fixtures/%s.expected', $fixture), $result);
}
diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected
index 8203714740752..06a118888aba9 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected
+++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected
@@ -12,6 +12,6 @@ Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: max-age=10800, private, must-revalidate
- [2] => Set-Cookie: sid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly
+ [2] => Set-Cookie: sid=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly
)
shutdown
diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.expected b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.expected
index 05a5d5d0b090f..549c6847f11da 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.expected
+++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.expected
@@ -16,6 +16,6 @@ Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: max-age=0, private, must-revalidate
- [2] => Set-Cookie: sid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly
+ [2] => Set-Cookie: sid=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly
)
shutdown
diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected
index 63078228df139..ac8ec061f0310 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected
+++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected
@@ -20,6 +20,6 @@ Array
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: max-age=10800, private, must-revalidate
[2] => Set-Cookie: abc=def
- [3] => Set-Cookie: sid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly
+ [3] => Set-Cookie: sid=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly
)
shutdown
diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php
index 972a2745132e1..a4f45fec68708 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php
@@ -12,6 +12,8 @@
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Proxy;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
+use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
/**
@@ -159,6 +161,23 @@ public function testUpdateTimestamp()
$this->proxy->updateTimestamp('id', 'data');
}
+
+ /**
+ * @dataProvider provideNativeSessionStorageHandler
+ */
+ public function testNativeSessionStorageSaveHandlerName($handler)
+ {
+ $this->assertSame('files', (new NativeSessionStorage([], $handler))->getSaveHandler()->getSaveHandlerName());
+ }
+
+ public function provideNativeSessionStorageHandler()
+ {
+ return [
+ [new \SessionHandler()],
+ [new StrictSessionHandler(new \SessionHandler())],
+ [new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler()))],
+ ];
+ }
}
abstract class TestSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json
index 8df36aa4d26e4..1b232eb5450cb 100644
--- a/src/Symfony/Component/HttpFoundation/composer.json
+++ b/src/Symfony/Component/HttpFoundation/composer.json
@@ -23,8 +23,11 @@
"require-dev": {
"predis/predis": "~1.0",
"symfony/cache": "^5.4|^6.0",
+ "symfony/dependency-injection": "^5.4|^6.0",
+ "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4",
"symfony/mime": "^5.4|^6.0",
- "symfony/expression-language": "^5.4|^6.0"
+ "symfony/expression-language": "^5.4|^6.0",
+ "symfony/rate-limiter": "^5.2|^6.0"
},
"suggest" : {
"symfony/mime": "To use the file extension guesser"
diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php
index 1460ec13dd068..bbc9321320ae8 100644
--- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php
+++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php
@@ -144,7 +144,7 @@ public function getFilters()
$allChannels = [];
foreach ($this->getProcessedLogs() as $log) {
- if ('' === trim($log['channel'])) {
+ if ('' === trim($log['channel'] ?? '')) {
continue;
}
diff --git a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php
index 4573acab8eaac..05be006e1722a 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php
@@ -153,6 +153,11 @@ public function onKernelResponse(ResponseEvent $event)
$isSessionEmpty = ($session instanceof Session ? $session->isEmpty() : empty($session->all())) && empty($_SESSION); // checking $_SESSION to keep compatibility with native sessions
if ($requestSessionCookieId && $isSessionEmpty) {
+ // PHP internally sets the session cookie value to "deleted" when setcookie() is called with empty string $value argument
+ // which happens in \Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler::destroy
+ // when the session gets invalidated (for example on logout) so we must handle this case here too
+ // otherwise we would send two Set-Cookie headers back with the response
+ SessionUtils::popSessionCookie($sessionName, 'deleted');
$response->headers->clearCookie(
$sessionName,
$sessionCookiePath,
diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php
index a5177f18fb5a6..f84a39e5a2af4 100644
--- a/src/Symfony/Component/HttpKernel/HttpKernel.php
+++ b/src/Symfony/Component/HttpKernel/HttpKernel.php
@@ -70,6 +70,7 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R
{
$request->headers->set('X-Php-Ob-Level', (string) ob_get_level());
+ $this->requestStack->push($request);
try {
return $this->handleRaw($request, $type);
} catch (\Exception $e) {
@@ -83,6 +84,8 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R
}
return $this->handleThrowable($e, $request, $type);
+ } finally {
+ $this->requestStack->pop();
}
}
@@ -121,8 +124,6 @@ public function terminateWithException(\Throwable $exception, Request $request =
*/
private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Response
{
- $this->requestStack->push($request);
-
// request
$event = new RequestEvent($this, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::REQUEST);
@@ -199,7 +200,6 @@ private function filterResponse(Response $response, Request $request, int $type)
private function finishRequest(Request $request, int $type)
{
$this->dispatcher->dispatch(new FinishRequestEvent($this, $request, $type), KernelEvents::FINISH_REQUEST);
- $this->requestStack->pop();
}
/**
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index 29c7aac0811ae..ec49d1a018616 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -78,11 +78,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
private static array $freshCache = [];
- public const VERSION = '6.1.3';
- public const VERSION_ID = 60103;
+ public const VERSION = '6.1.4';
+ public const VERSION_ID = 60104;
public const MAJOR_VERSION = 6;
public const MINOR_VERSION = 1;
- public const RELEASE_VERSION = 3;
+ public const RELEASE_VERSION = 4;
public const EXTRA_VERSION = '';
public const END_OF_MAINTENANCE = '01/2023';
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php
index 91636ba86ca6a..182b524e98bcf 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php
@@ -40,6 +40,45 @@ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrue()
$kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true);
}
+ public function testRequestStackIsNotBrokenWhenControllerThrowsAnExceptionAndCatchIsTrue()
+ {
+ $requestStack = new RequestStack();
+ $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \RuntimeException(); }, $requestStack);
+
+ try {
+ $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true);
+ } catch (\Throwable $exception) {
+ }
+
+ self::assertNull($requestStack->getCurrentRequest());
+ }
+
+ public function testRequestStackIsNotBrokenWhenControllerThrowsAnExceptionAndCatchIsFalse()
+ {
+ $requestStack = new RequestStack();
+ $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \RuntimeException(); }, $requestStack);
+
+ try {
+ $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, false);
+ } catch (\Throwable $exception) {
+ }
+
+ self::assertNull($requestStack->getCurrentRequest());
+ }
+
+ public function testRequestStackIsNotBrokenWhenControllerThrowsAnThrowable()
+ {
+ $requestStack = new RequestStack();
+ $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \Error(); }, $requestStack);
+
+ try {
+ $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true);
+ } catch (\Throwable $exception) {
+ }
+
+ self::assertNull($requestStack->getCurrentRequest());
+ }
+
public function testHandleWhenControllerThrowsAnExceptionAndCatchIsFalseAndNoListenerIsRegistered()
{
$this->expectException(\RuntimeException::class);
diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php
index 9a5b214590047..61b3a0e157b17 100644
--- a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php
+++ b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php
@@ -114,8 +114,16 @@ private function doEhloCommand(): string
{
try {
$response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
- } catch (TransportExceptionInterface) {
- return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]);
+ } catch (TransportExceptionInterface $e) {
+ try {
+ return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]);
+ } catch (TransportExceptionInterface $ex) {
+ if (!$ex->getCode()) {
+ throw $e;
+ }
+
+ throw $ex;
+ }
}
$this->capabilities = $this->parseCapabilities($response);
@@ -132,12 +140,8 @@ private function doEhloCommand(): string
throw new TransportException('Unable to connect with STARTTLS.');
}
- try {
- $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
- $this->capabilities = $this->parseCapabilities($response);
- } catch (TransportExceptionInterface) {
- return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]);
- }
+ $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
+ $this->capabilities = $this->parseCapabilities($response);
}
if (\array_key_exists('AUTH', $this->capabilities)) {
diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php
index 9432be7f3645f..605bddbf83e07 100644
--- a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php
+++ b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php
@@ -306,15 +306,13 @@ private function assertResponseCode(string $response, array $codes): void
throw new LogicException('You must set the expected response code.');
}
- if (!$response) {
- throw new TransportException(sprintf('Expected response code "%s" but got an empty response.', implode('/', $codes)));
- }
-
[$code] = sscanf($response, '%3d');
$valid = \in_array($code, $codes);
- if (!$valid) {
- throw new TransportException(sprintf('Expected response code "%s" but got code "%s", with message "%s".', implode('/', $codes), $code, trim($response)), $code);
+ if (!$valid || !$response) {
+ $codeStr = $code ? sprintf('code "%s"', $code) : 'empty code';
+ $responseStr = $response ? sprintf(', with message "%s"', trim($response)) : '';
+ throw new TransportException(sprintf('Expected response code "%s" but got ', implode('/', $codes), $codeStr).$codeStr.$responseStr.'.', $code);
}
}
diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/PostgreSqlConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/PostgreSqlConnectionTest.php
index f1ffffbb5687a..e8e00d97b3876 100644
--- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/PostgreSqlConnectionTest.php
+++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/PostgreSqlConnectionTest.php
@@ -11,6 +11,11 @@
namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport;
+use Doctrine\DBAL\Cache\ArrayResult;
+use Doctrine\DBAL\Cache\ArrayStatement;
+use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
+use Doctrine\DBAL\Query\QueryBuilder;
+use Doctrine\DBAL\Result;
use Doctrine\DBAL\Schema\Table;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\PostgreSqlConnection;
@@ -42,6 +47,68 @@ public function testUnserialize()
$connection->__wakeup();
}
+ public function testListenOnConnection()
+ {
+ $driverConnection = $this->createMock(\Doctrine\DBAL\Connection::class);
+
+ $driverConnection
+ ->expects(self::any())
+ ->method('getDatabasePlatform')
+ ->willReturn(new PostgreSQLPlatform());
+
+ $driverConnection
+ ->expects(self::any())
+ ->method('createQueryBuilder')
+ ->willReturn(new QueryBuilder($driverConnection));
+
+ $wrappedConnection = new class() {
+ private $notifyCalls = 0;
+
+ public function pgsqlGetNotify()
+ {
+ ++$this->notifyCalls;
+
+ return false;
+ }
+
+ public function countNotifyCalls()
+ {
+ return $this->notifyCalls;
+ }
+ };
+
+ // dbal 2.x
+ if (interface_exists(Result::class)) {
+ $driverConnection
+ ->expects(self::exactly(2))
+ ->method('getWrappedConnection')
+ ->willReturn($wrappedConnection);
+
+ $driverConnection
+ ->expects(self::any())
+ ->method('executeQuery')
+ ->willReturn(new ArrayStatement([]));
+ } else {
+ // dbal 3.x
+ $driverConnection
+ ->expects(self::exactly(2))
+ ->method('getNativeConnection')
+ ->willReturn($wrappedConnection);
+
+ $driverConnection
+ ->expects(self::any())
+ ->method('executeQuery')
+ ->willReturn(new Result(new ArrayResult([]), $driverConnection));
+ }
+ $connection = new PostgreSqlConnection(['table_name' => 'queue_table'], $driverConnection);
+
+ $connection->get(); // first time we have queueEmptiedAt === null, fallback on the parent implementation
+ $connection->get();
+ $connection->get();
+
+ $this->assertSame(2, $wrappedConnection->countNotifyCalls());
+ }
+
public function testGetExtraSetupSql()
{
$driverConnection = $this->createMock(\Doctrine\DBAL\Connection::class);
diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php
index 8592d112c639a..e7bf744d3100e 100644
--- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php
+++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php
@@ -158,7 +158,7 @@ public function get(): ?array
{
if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) {
try {
- $this->driverConnection->delete($this->configuration['table_name'], ['delivered_at' => '9999-12-31']);
+ $this->driverConnection->delete($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59']);
} catch (DriverException $e) {
// Ignore the exception
}
@@ -252,7 +252,7 @@ public function ack(string $id): bool
{
try {
if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) {
- return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31'], ['id' => $id]) > 0;
+ return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59'], ['id' => $id]) > 0;
}
return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0;
@@ -265,7 +265,7 @@ public function reject(string $id): bool
{
try {
if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) {
- return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31'], ['id' => $id]) > 0;
+ return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59'], ['id' => $id]) > 0;
}
return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0;
diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php
index fbfe3ef618653..3691a9383f293 100644
--- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php
+++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php
@@ -33,8 +33,6 @@ final class PostgreSqlConnection extends Connection
'get_notify_timeout' => 0,
];
- private bool $listening = false;
-
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
@@ -62,12 +60,9 @@ public function get(): ?array
return parent::get();
}
- if (!$this->listening) {
- // This is secure because the table name must be a valid identifier:
- // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
- $this->executeStatement(sprintf('LISTEN "%s"', $this->configuration['table_name']));
- $this->listening = true;
- }
+ // This is secure because the table name must be a valid identifier:
+ // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
+ $this->executeStatement(sprintf('LISTEN "%s"', $this->configuration['table_name']));
if (method_exists($this->driverConnection, 'getNativeConnection')) {
$wrappedConnection = $this->driverConnection->getNativeConnection();
@@ -150,11 +145,6 @@ private function createTriggerFunctionName(): string
private function unlisten()
{
- if (!$this->listening) {
- return;
- }
-
$this->executeStatement(sprintf('UNLISTEN "%s"', $this->configuration['table_name']));
- $this->listening = false;
}
}
diff --git a/src/Symfony/Component/Mime/Email.php b/src/Symfony/Component/Mime/Email.php
index 2aa63f7d01ab8..15f1918ffb24b 100644
--- a/src/Symfony/Component/Mime/Email.php
+++ b/src/Symfony/Component/Mime/Email.php
@@ -490,8 +490,8 @@ private function prepareParts(): ?array
$html = $htmlPart->getBody();
$regexes = [
- '
]*src\s*=\s*(?:([\'"])cid:([^"]+)\\1|cid:([^>\s]+))',
- '<\w+\s+[^>]*background\s*=\s*(?:([\'"])cid:([^"]+)\\1|cid:([^>\s]+))',
+ '
]*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))',
+ '<\w+\s+[^>]*background\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))',
];
$tmpMatches = [];
foreach ($regexes as $regex) {
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeChat/Tests/FakeChatEmailTransportTest.php b/src/Symfony/Component/Notifier/Bridge/FakeChat/Tests/FakeChatEmailTransportTest.php
index b102927fb8db2..74d9c167f94b8 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeChat/Tests/FakeChatEmailTransportTest.php
+++ b/src/Symfony/Component/Notifier/Bridge/FakeChat/Tests/FakeChatEmailTransportTest.php
@@ -119,7 +119,7 @@ public function testSendWithCustomTransportAndWithRecipient()
$this->assertSame(sprintf('New Chat message for recipient: %s', $recipient), $sentEmail->getSubject());
$this->assertSame($subject, $sentEmail->getTextBody());
$this->assertTrue($sentEmail->getHeaders()->has('X-Transport'));
- $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBodyAsString());
+ $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBody());
}
public function testSendWithCustomTransportAndWithoutRecipient()
@@ -143,6 +143,6 @@ public function testSendWithCustomTransportAndWithoutRecipient()
$this->assertSame('New Chat message without specified recipient!', $sentEmail->getSubject());
$this->assertSame($subject, $sentEmail->getTextBody());
$this->assertTrue($sentEmail->getHeaders()->has('X-Transport'));
- $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBodyAsString());
+ $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBody());
}
}
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/Tests/FakeSmsEmailTransportTest.php b/src/Symfony/Component/Notifier/Bridge/FakeSms/Tests/FakeSmsEmailTransportTest.php
index 3b0e86e3ef613..f5efc9573dac0 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeSms/Tests/FakeSmsEmailTransportTest.php
+++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/Tests/FakeSmsEmailTransportTest.php
@@ -96,6 +96,6 @@ public function testSendWithCustomTransport()
$this->assertSame(sprintf('New SMS on phone number: %s', $phone), $sentEmail->getSubject());
$this->assertSame($subject, $sentEmail->getTextBody());
$this->assertTrue($sentEmail->getHeaders()->has('X-Transport'));
- $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBodyAsString());
+ $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBody());
}
}
diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php
index 92df675f98c3f..6b0a737824a67 100644
--- a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php
+++ b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php
@@ -48,7 +48,7 @@ public function getRoleNames(): array
public function getUserIdentifier(): string
{
- return $this->user->getUserIdentifier();
+ return $this->user ? $this->user->getUserIdentifier() : '';
}
/**
diff --git a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
index 1594c1b38cab9..b2b131d08a63a 100644
--- a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
+++ b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
@@ -40,8 +40,6 @@ public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionM
/**
* {@inheritdoc}
- *
- * @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token and $exceptionOnNoToken is set to true
*/
final public function isGranted(mixed $attribute, mixed $subject = null): bool
{
diff --git a/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php b/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php
index c17e9e4adc6c9..7945b417fdf49 100644
--- a/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php
+++ b/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php
@@ -37,12 +37,12 @@ public function __construct(string $userFqcn, string $userIdentifier, int $expir
public static function fromRawCookie(string $rawCookie): self
{
$cookieParts = explode(self::COOKIE_DELIMITER, base64_decode($rawCookie), 4);
- if (false === $cookieParts[1] = base64_decode($cookieParts[1], true)) {
- throw new AuthenticationException('The user identifier contains a character from outside the base64 alphabet.');
- }
if (4 !== \count($cookieParts)) {
throw new AuthenticationException('The cookie contains invalid data.');
}
+ if (false === $cookieParts[1] = base64_decode($cookieParts[1], true)) {
+ throw new AuthenticationException('The user identifier contains a character from outside the base64 alphabet.');
+ }
return new static(...$cookieParts);
}
diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php
index 406d48c164add..c7492a95a464f 100644
--- a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php
@@ -89,4 +89,12 @@ public function testAuthenticateWithoutOldToken()
$request = Request::create('/', 'GET', [], ['_remember_me_cookie' => base64_encode('foo:bar')]);
$this->authenticator->authenticate($request);
}
+
+ public function testAuthenticateWithTokenWithoutDelimiter()
+ {
+ $this->expectException(AuthenticationException::class);
+
+ $request = Request::create('/', 'GET', [], ['_remember_me_cookie' => 'invalid']);
+ $this->authenticator->authenticate($request);
+ }
}
diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md
index c2b3ebfb3c6c2..0a475a0eafb9c 100644
--- a/src/Symfony/Component/Serializer/CHANGELOG.md
+++ b/src/Symfony/Component/Serializer/CHANGELOG.md
@@ -9,8 +9,6 @@ CHANGELOG
* Set `Context` annotation as not final
* Deprecate `ContextAwareNormalizerInterface`, use `NormalizerInterface` instead
* Deprecate `ContextAwareDenormalizerInterface`, use `DenormalizerInterface` instead
- * Deprecate `ContextAwareEncoderInterface`, use `EncoderInterface` instead
- * Deprecate `ContextAwareDecoderInterface`, use `DecoderInterface` instead
* Deprecate supporting denormalization for `AbstractUid` in `UidNormalizer`, use one of `AbstractUid` child class instead
* Deprecate denormalizing to an abstract class in `UidNormalizer`
* Add support for `can*()` methods to `ObjectNormalizer`
diff --git a/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php b/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php
index 0f7f94fad8842..7a01938ab4d52 100644
--- a/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php
@@ -67,9 +67,13 @@ private function getDecoder(string $format, array $context): DecoderInterface
return $this->decoders[$this->decoderByFormat[$format]];
}
+ $cache = true;
foreach ($this->decoders as $i => $decoder) {
+ $cache = $cache && !$decoder instanceof ContextAwareDecoderInterface;
if ($decoder->supportsDecoding($format, $context)) {
- $this->decoderByFormat[$format] = $i;
+ if ($cache) {
+ $this->decoderByFormat[$format] = $i;
+ }
return $decoder;
}
diff --git a/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php b/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php
index 70d7c9fd10645..061a9cf748bd8 100644
--- a/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php
@@ -90,9 +90,13 @@ private function getEncoder(string $format, array $context): EncoderInterface
return $this->encoders[$this->encoderByFormat[$format]];
}
+ $cache = true;
foreach ($this->encoders as $i => $encoder) {
+ $cache = $cache && !$encoder instanceof ContextAwareEncoderInterface;
if ($encoder->supportsEncoding($format, $context)) {
- $this->encoderByFormat[$format] = $i;
+ if ($cache) {
+ $this->encoderByFormat[$format] = $i;
+ }
return $encoder;
}
diff --git a/src/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.php b/src/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.php
index 910b26bac1fc8..6ac2e38cc4657 100644
--- a/src/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.php
+++ b/src/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.php
@@ -15,8 +15,6 @@
* Adds the support of an extra $context parameter for the supportsDecoding method.
*
* @author Kévin Dunglas
- *
- * @deprecated since symfony/serializer 6.1, use DecoderInterface instead
*/
interface ContextAwareDecoderInterface extends DecoderInterface
{
diff --git a/src/Symfony/Component/Serializer/Encoder/ContextAwareEncoderInterface.php b/src/Symfony/Component/Serializer/Encoder/ContextAwareEncoderInterface.php
index f828f87a4f82f..832b600eeca57 100644
--- a/src/Symfony/Component/Serializer/Encoder/ContextAwareEncoderInterface.php
+++ b/src/Symfony/Component/Serializer/Encoder/ContextAwareEncoderInterface.php
@@ -15,8 +15,6 @@
* Adds the support of an extra $context parameter for the supportsEncoding method.
*
* @author Kévin Dunglas
- *
- * @deprecated since symfony/serializer 6.1, use EncoderInterface instead
*/
interface ContextAwareEncoderInterface extends EncoderInterface
{
diff --git a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
index b2c6fcd81d4ae..a3733a53dee24 100644
--- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
@@ -124,10 +124,8 @@ public function encode(mixed $data, string $format, array $context = []): string
/**
* {@inheritdoc}
- *
- * @param array $context
*/
- public function supportsEncoding(string $format /* , array $context = [] */): bool
+ public function supportsEncoding(string $format): bool
{
return self::FORMAT === $format;
}
@@ -212,10 +210,8 @@ public function decode(string $data, string $format, array $context = []): mixed
/**
* {@inheritdoc}
- *
- * @param array $context
*/
- public function supportsDecoding(string $format /* , array $context = [] */): bool
+ public function supportsDecoding(string $format): bool
{
return self::FORMAT === $format;
}
diff --git a/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php b/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php
index 5014b9bd514ab..84a84ad1f3e69 100644
--- a/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php
+++ b/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php
@@ -39,10 +39,9 @@ public function decode(string $data, string $format, array $context = []);
/**
* Checks whether the deserializer can decode from given format.
*
- * @param string $format Format name
- * @param array $context Options that decoders have access to
+ * @param string $format Format name
*
* @return bool
*/
- public function supportsDecoding(string $format /* , array $context = [] */);
+ public function supportsDecoding(string $format);
}
diff --git a/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php b/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php
index c913ac3fb14ad..e0f303b1e3dcd 100644
--- a/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php
+++ b/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php
@@ -32,8 +32,7 @@ public function encode(mixed $data, string $format, array $context = []): string
/**
* Checks whether the serializer can encode to given format.
*
- * @param string $format Format name
- * @param array $context Options that normalizers/encoders have access to
+ * @param string $format Format name
*/
- public function supportsEncoding(string $format /* , array $context = [] */): bool;
+ public function supportsEncoding(string $format): bool;
}
diff --git a/src/Symfony/Component/Serializer/Encoder/JsonDecode.php b/src/Symfony/Component/Serializer/Encoder/JsonDecode.php
index 50d2d2e3f266f..ad094afaca161 100644
--- a/src/Symfony/Component/Serializer/Encoder/JsonDecode.php
+++ b/src/Symfony/Component/Serializer/Encoder/JsonDecode.php
@@ -95,10 +95,8 @@ public function decode(string $data, string $format, array $context = []): mixed
/**
* {@inheritdoc}
- *
- * @param array $context
*/
- public function supportsDecoding(string $format /* , array $context = [] */): bool
+ public function supportsDecoding(string $format): bool
{
return JsonEncoder::FORMAT === $format;
}
diff --git a/src/Symfony/Component/Serializer/Encoder/JsonEncode.php b/src/Symfony/Component/Serializer/Encoder/JsonEncode.php
index 86baf99994eb6..23d0fdd960e3e 100644
--- a/src/Symfony/Component/Serializer/Encoder/JsonEncode.php
+++ b/src/Symfony/Component/Serializer/Encoder/JsonEncode.php
@@ -57,10 +57,8 @@ public function encode(mixed $data, string $format, array $context = []): string
/**
* {@inheritdoc}
- *
- * @param array $context
*/
- public function supportsEncoding(string $format /* , array $context = [] */): bool
+ public function supportsEncoding(string $format): bool
{
return JsonEncoder::FORMAT === $format;
}
diff --git a/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php b/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php
index e6ccbfba50b2b..d17ef049285ef 100644
--- a/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php
@@ -47,20 +47,16 @@ public function decode(string $data, string $format, array $context = []): mixed
/**
* {@inheritdoc}
- *
- * @param array $context
*/
- public function supportsEncoding(string $format /* , array $context = [] */): bool
+ public function supportsEncoding(string $format): bool
{
return self::FORMAT === $format;
}
/**
* {@inheritdoc}
- *
- * @param array $context
*/
- public function supportsDecoding(string $format /* , array $context = [] */): bool
+ public function supportsDecoding(string $format): bool
{
return self::FORMAT === $format;
}
diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php
index a47e6d69583f3..2474f4439a443 100644
--- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php
@@ -166,20 +166,16 @@ public function decode(string $data, string $format, array $context = []): mixed
/**
* {@inheritdoc}
- *
- * @param array $context
*/
- public function supportsEncoding(string $format /* , array $context = [] */): bool
+ public function supportsEncoding(string $format): bool
{
return self::FORMAT === $format;
}
/**
* {@inheritdoc}
- *
- * @param array $context
*/
- public function supportsDecoding(string $format /* , array $context = [] */): bool
+ public function supportsDecoding(string $format): bool
{
return self::FORMAT === $format;
}
diff --git a/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php b/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php
index ecb9815eee553..51f600786aa3b 100644
--- a/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php
@@ -67,10 +67,8 @@ public function encode(mixed $data, string $format, array $context = []): string
/**
* {@inheritdoc}
- *
- * @param array $context
*/
- public function supportsEncoding(string $format /* , array $context = [] */): bool
+ public function supportsEncoding(string $format): bool
{
return self::FORMAT === $format || self::ALTERNATIVE_FORMAT === $format;
}
@@ -87,10 +85,8 @@ public function decode(string $data, string $format, array $context = []): mixed
/**
* {@inheritdoc}
- *
- * @param array $context
*/
- public function supportsDecoding(string $format /* , array $context = [] */): bool
+ public function supportsDecoding(string $format): bool
{
return self::FORMAT === $format || self::ALTERNATIVE_FORMAT === $format;
}
diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php
index f6c196f385717..0e534a4809f76 100644
--- a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php
+++ b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php
@@ -100,8 +100,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
continue;
}
- $getAccessor = preg_match('/^(get|)(.+)$/i', $method->name);
- if ($getAccessor && 0 !== $method->getNumberOfRequiredParameters()) {
+ if (0 === stripos($method->name, 'get') && $method->getNumberOfRequiredParameters()) {
continue; /* matches the BC behavior in `Symfony\Component\Serializer\Normalizer\ObjectNormalizer::extractAttributes` */
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
index 44ba45f581a19..aa5e808179b3a 100644
--- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
@@ -387,7 +387,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
}
$exception = NotNormalizableValueException::createForUnexpectedDataType(
- sprintf('Failed to create object because the object miss the "%s" property.', $constructorParameter->name),
+ sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name),
$data,
['unknown'],
$context['deserialization_path'] ?? null,
diff --git a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php
index a212704b7c596..8fa797487a456 100644
--- a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Serializer\Normalizer;
+use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Exception\BadMethodCallException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -37,7 +38,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!');
}
if (!\is_array($data)) {
- throw new InvalidArgumentException('Data expected to be an array, '.get_debug_type($data).' given.');
+ throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, [Type::BUILTIN_TYPE_ARRAY], $context['deserialization_path'] ?? null);
}
if (!str_ends_with($type, '[]')) {
throw new InvalidArgumentException('Unsupported class: '.$type);
diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php
index 859a09362d3f0..8aa242d67d5df 100644
--- a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php
@@ -25,7 +25,7 @@ final class BackedEnumNormalizer implements NormalizerInterface, DenormalizerInt
/**
* {@inheritdoc}
*/
- public function normalize($object, $format = null, array $context = []): int|string
+ public function normalize(mixed $object, string $format = null, array $context = []): int|string
{
if (!$object instanceof \BackedEnum) {
throw new InvalidArgumentException('The data must belong to a backed enumeration.');
@@ -37,7 +37,7 @@ public function normalize($object, $format = null, array $context = []): int|str
/**
* {@inheritdoc}
*/
- public function supportsNormalization($data, $format = null, array $context = []): bool
+ public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
{
return $data instanceof \BackedEnum;
}
@@ -47,7 +47,7 @@ public function supportsNormalization($data, $format = null, array $context = []
*
* @throws NotNormalizableValueException
*/
- public function denormalize($data, $type, $format = null, array $context = []): mixed
+ public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
if (!is_subclass_of($type, \BackedEnum::class)) {
throw new InvalidArgumentException('The data must belong to a backed enumeration.');
@@ -60,14 +60,14 @@ public function denormalize($data, $type, $format = null, array $context = []):
try {
return $type::from($data);
} catch (\ValueError $e) {
- throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
+ throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type);
}
}
/**
* {@inheritdoc}
*/
- public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
+ public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return is_subclass_of($type, \BackedEnum::class);
}
diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php
index 5cac8d99a5270..8f433ce0fa15a 100644
--- a/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php
@@ -13,6 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Encoder\ChainDecoder;
+use Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Exception\RuntimeException;
@@ -28,7 +29,7 @@ class ChainDecoderTest extends TestCase
protected function setUp(): void
{
- $this->decoder1 = $this->createMock(DecoderInterface::class);
+ $this->decoder1 = $this->createMock(ContextAwareDecoderInterface::class);
$this->decoder1
->method('supportsDecoding')
->willReturnMap([
@@ -36,6 +37,7 @@ protected function setUp(): void
[self::FORMAT_2, [], false],
[self::FORMAT_3, [], false],
[self::FORMAT_3, ['foo' => 'bar'], true],
+ [self::FORMAT_3, ['foo' => 'bar2'], false],
]);
$this->decoder2 = $this->createMock(DecoderInterface::class);
@@ -45,6 +47,8 @@ protected function setUp(): void
[self::FORMAT_1, [], false],
[self::FORMAT_2, [], true],
[self::FORMAT_3, [], false],
+ [self::FORMAT_3, ['foo' => 'bar'], false],
+ [self::FORMAT_3, ['foo' => 'bar2'], true],
]);
$this->chainDecoder = new ChainDecoder([$this->decoder1, $this->decoder2]);
@@ -52,10 +56,26 @@ protected function setUp(): void
public function testSupportsDecoding()
{
+ $this->decoder1
+ ->method('decode')
+ ->willReturn('result1');
+ $this->decoder2
+ ->method('decode')
+ ->willReturn('result2');
+
$this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_1));
+ $this->assertEquals('result1', $this->chainDecoder->decode('', self::FORMAT_1, []));
+
$this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_2));
+ $this->assertEquals('result2', $this->chainDecoder->decode('', self::FORMAT_2, []));
+
$this->assertFalse($this->chainDecoder->supportsDecoding(self::FORMAT_3));
+
$this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_3, ['foo' => 'bar']));
+ $this->assertEquals('result1', $this->chainDecoder->decode('', self::FORMAT_3, ['foo' => 'bar']));
+
+ $this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_3, ['foo' => 'bar2']));
+ $this->assertEquals('result2', $this->chainDecoder->decode('', self::FORMAT_3, ['foo' => 'bar2']));
}
public function testDecode()
diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php
index 848087145bafe..0afd67813435b 100644
--- a/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Debug\TraceableEncoder;
use Symfony\Component\Serializer\Encoder\ChainEncoder;
+use Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Encoder\NormalizationAwareInterface;
use Symfony\Component\Serializer\Exception\RuntimeException;
@@ -30,7 +31,7 @@ class ChainEncoderTest extends TestCase
protected function setUp(): void
{
- $this->encoder1 = $this->createMock(EncoderInterface::class);
+ $this->encoder1 = $this->createMock(ContextAwareEncoderInterface::class);
$this->encoder1
->method('supportsEncoding')
->willReturnMap([
@@ -38,6 +39,7 @@ protected function setUp(): void
[self::FORMAT_2, [], false],
[self::FORMAT_3, [], false],
[self::FORMAT_3, ['foo' => 'bar'], true],
+ [self::FORMAT_3, ['foo' => 'bar2'], false],
]);
$this->encoder2 = $this->createMock(EncoderInterface::class);
@@ -47,6 +49,8 @@ protected function setUp(): void
[self::FORMAT_1, [], false],
[self::FORMAT_2, [], true],
[self::FORMAT_3, [], false],
+ [self::FORMAT_3, ['foo' => 'bar'], false],
+ [self::FORMAT_3, ['foo' => 'bar2'], true],
]);
$this->chainEncoder = new ChainEncoder([$this->encoder1, $this->encoder2]);
@@ -54,10 +58,26 @@ protected function setUp(): void
public function testSupportsEncoding()
{
+ $this->encoder1
+ ->method('encode')
+ ->willReturn('result1');
+ $this->encoder2
+ ->method('encode')
+ ->willReturn('result2');
+
$this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_1));
+ $this->assertEquals('result1', $this->chainEncoder->encode('', self::FORMAT_1, []));
+
$this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_2));
+ $this->assertEquals('result2', $this->chainEncoder->encode('', self::FORMAT_2, []));
+
$this->assertFalse($this->chainEncoder->supportsEncoding(self::FORMAT_3));
+
$this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_3, ['foo' => 'bar']));
+ $this->assertEquals('result1', $this->chainEncoder->encode('', self::FORMAT_3, ['foo' => 'bar']));
+
+ $this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_3, ['foo' => 'bar2']));
+ $this->assertEquals('result2', $this->chainEncoder->encode('', self::FORMAT_3, ['foo' => 'bar2']));
}
public function testEncode()
@@ -106,7 +126,7 @@ public function testNeedsNormalizationTraceableEncoder()
class NormalizationAwareEncoder implements EncoderInterface, NormalizationAwareInterface
{
- public function supportsEncoding(string $format, array $context = []): bool
+ public function supportsEncoding(string $format): bool
{
return true;
}
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetter.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetter.php
index a2fe769e36b8c..326a9cd07589e 100644
--- a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetter.php
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetter.php
@@ -20,4 +20,8 @@ public function getExtraValue(string $parameter)
{
return $parameter;
}
+
+ public function setExtraValue2(string $parameter)
+ {
+ }
}
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php
index 11094dad012e6..2b717c93a9752 100644
--- a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php
@@ -15,4 +15,8 @@ public function getExtraValue(string $parameter)
{
return $parameter;
}
+
+ public function setExtraValue2(string $parameter)
+ {
+ }
}
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetter.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetter.php
index cec21db4be663..274479e63b5b3 100644
--- a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetter.php
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetter.php
@@ -18,4 +18,8 @@ public function getExtraValue(string $parameter)
{
return $parameter;
}
+
+ public function setExtraValue2(string $parameter)
+ {
+ }
}
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php
index 6f0f6da1bb883..21abb870be477 100644
--- a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php
@@ -15,4 +15,8 @@ public function getExtraValue(string $parameter)
{
return $parameter;
}
+
+ public function setExtraValue2(string $parameter)
+ {
+ }
}
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumConstructor.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumConstructor.php
new file mode 100644
index 0000000000000..be5ea3cff0ece
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumConstructor.php
@@ -0,0 +1,12 @@
+getAttributesMetadata();
self::assertArrayNotHasKey('extraValue', $attributes);
+ self::assertArrayHasKey('extraValue2', $attributes);
}
public function testIgnoreGetterWirhRequiredParameterIfIgnoreAnnotationIsNotUsed()
@@ -163,6 +164,7 @@ public function testIgnoreGetterWirhRequiredParameterIfIgnoreAnnotationIsNotUsed
$attributes = $classMetadata->getAttributesMetadata();
self::assertArrayNotHasKey('extraValue', $attributes);
+ self::assertArrayHasKey('extraValue2', $attributes);
}
abstract protected function createLoader(): AnnotationLoader;
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php
index 8ea2ac2b0f935..b0063da5fe4e7 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php
@@ -88,8 +88,9 @@ public function testDenormalizeObjectThrowsException()
public function testDenormalizeBadBackingValueThrowsException()
{
- $this->expectException(NotNormalizableValueException::class);
- $this->expectExceptionMessage('"POST" is not a valid backing value for enum "'.StringBackedEnumDummy::class.'"');
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The data must belong to a backed enumeration of type '.StringBackedEnumDummy::class);
+
$this->normalizer->denormalize('POST', StringBackedEnumDummy::class);
}
diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php
index b76e7d250f16b..9fb1519b8bae7 100644
--- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php
@@ -36,6 +36,7 @@
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
+use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
use Symfony\Component\Serializer\Normalizer\CustomNormalizer;
use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
@@ -58,6 +59,7 @@
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface;
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne;
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo;
+use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor;
use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy;
use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Php74Full;
@@ -852,7 +854,8 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet
},
"nestedObject": {
"int": "string"
- }
+ },
+ "anotherCollection": null
}';
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
@@ -998,7 +1001,7 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet
],
'path' => 'php74FullWithConstructor',
'useMessageForUser' => true,
- 'message' => 'Failed to create object because the object miss the "constructorArgument" property.',
+ 'message' => 'Failed to create object because the class misses the "constructorArgument" property.',
],
$classMetadataFactory ?
[
@@ -1028,6 +1031,13 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet
'useMessageForUser' => true,
'message' => 'The type of the key "int" must be "int" ("string" given).',
],
+ [
+ 'currentType' => 'null',
+ 'expectedTypes' => ['array'],
+ 'path' => 'anotherCollection',
+ 'useMessageForUser' => false,
+ 'message' => 'Data expected to be "Symfony\Component\Serializer\Tests\Fixtures\Php74Full[]", "null" given.',
+ ],
];
$this->assertSame($expected, $exceptionsAsArray);
@@ -1159,6 +1169,69 @@ public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFa
$this->assertSame($expected, $exceptionsAsArray);
}
+ /**
+ * @requires PHP 8.1
+ */
+ public function testCollectDenormalizationErrorsWithEnumConstructor()
+ {
+ $serializer = new Serializer(
+ [
+ new BackedEnumNormalizer(),
+ new ObjectNormalizer(),
+ ],
+ ['json' => new JsonEncoder()]
+ );
+
+ try {
+ $serializer->deserialize('{"invalid": "GET"}', DummyObjectWithEnumConstructor::class, 'json', [
+ DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
+ ]);
+ } catch (\Throwable $th) {
+ $this->assertInstanceOf(PartialDenormalizationException::class, $th);
+ }
+
+ $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array {
+ return [
+ 'currentType' => $e->getCurrentType(),
+ 'useMessageForUser' => $e->canUseMessageForUser(),
+ 'message' => $e->getMessage(),
+ ];
+ }, $th->getErrors());
+
+ $expected = [
+ [
+ 'currentType' => 'array',
+ 'useMessageForUser' => true,
+ 'message' => 'Failed to create object because the class misses the "get" property.',
+ ],
+ ];
+
+ $this->assertSame($expected, $exceptionsAsArray);
+ }
+
+ /**
+ * @requires PHP 8.1
+ */
+ public function testNoCollectDenormalizationErrorsWithWrongEnum()
+ {
+ $serializer = new Serializer(
+ [
+ new BackedEnumNormalizer(),
+ new ObjectNormalizer(),
+ ],
+ ['json' => new JsonEncoder()]
+ );
+
+ try {
+ $serializer->deserialize('{"get": "invalid"}', DummyObjectWithEnumConstructor::class, 'json', [
+ DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
+ ]);
+ } catch (\Throwable $th) {
+ $this->assertNotInstanceOf(PartialDenormalizationException::class, $th);
+ $this->assertInstanceOf(InvalidArgumentException::class, $th);
+ }
+ }
+
public function provideCollectDenormalizationErrors()
{
return [
diff --git a/src/Symfony/Component/String/AbstractString.php b/src/Symfony/Component/String/AbstractString.php
index fcab13af8947e..2932212751ce6 100644
--- a/src/Symfony/Component/String/AbstractString.php
+++ b/src/Symfony/Component/String/AbstractString.php
@@ -244,7 +244,7 @@ abstract public function chunk(int $length = 1): array;
public function collapseWhitespace(): static
{
$str = clone $this;
- $str->string = trim(preg_replace('/(?:\s{2,}+|[^\S ])/', ' ', $str->string));
+ $str->string = trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $str->string), " \n\r\t\x0C");
return $str;
}
diff --git a/src/Symfony/Component/String/AbstractUnicodeString.php b/src/Symfony/Component/String/AbstractUnicodeString.php
index 5f8859fef2ae1..5152fd366c1cb 100644
--- a/src/Symfony/Component/String/AbstractUnicodeString.php
+++ b/src/Symfony/Component/String/AbstractUnicodeString.php
@@ -356,7 +356,7 @@ public function reverse(): static
public function snake(): static
{
- $str = $this->camel()->title();
+ $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');
return $str;
diff --git a/src/Symfony/Component/String/ByteString.php b/src/Symfony/Component/String/ByteString.php
index 21bbf4a670471..ca4cf95b8887e 100644
--- a/src/Symfony/Component/String/ByteString.php
+++ b/src/Symfony/Component/String/ByteString.php
@@ -347,7 +347,7 @@ public function slice(int $start = 0, int $length = null): static
public function snake(): static
{
- $str = $this->camel()->title();
+ $str = $this->camel();
$str->string = 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/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php
index d28cfb6f53d78..b3c3d9086e1e6 100644
--- a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php
+++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php
@@ -1041,6 +1041,7 @@ public static function provideCamel()
{
return [
['', ''],
+ ['xY', 'x_y'],
['symfonyIsGreat', 'symfony_is_great'],
['symfony5IsGreat', 'symfony_5_is_great'],
['symfonyIsGreat', 'Symfony is great'],
@@ -1063,6 +1064,8 @@ public static function provideSnake()
{
return [
['', ''],
+ ['x_y', 'x_y'],
+ ['x_y', 'X_Y'],
['symfony_is_great', 'symfonyIsGreat'],
['symfony5_is_great', 'symfony5IsGreat'],
['symfony5is_great', 'symfony5isGreat'],
diff --git a/src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php b/src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php
new file mode 100644
index 0000000000000..d58c002c40d99
--- /dev/null
+++ b/src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\String;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\String\Slugger\AsciiSlugger;
+
+class AsciiSluggerTest extends TestCase
+{
+ public function provideSlugTests(): iterable
+ {
+ yield ['', ''];
+ yield ['foo', ' foo '];
+ yield ['foo-bar', 'foo bar'];
+
+ yield ['foo-bar', 'foo@bar', '-'];
+ yield ['foo-at-bar', 'foo@bar', '-', 'en'];
+
+ yield ['e-a', 'é$!à'];
+ yield ['e_a', 'é$!à', '_'];
+
+ yield ['a', 'ä'];
+ yield ['a', 'ä', '-', 'fr'];
+ yield ['ae', 'ä', '-', 'de'];
+ yield ['ae', 'ä', '-', 'de_fr']; // Ensure we get the parent locale
+ yield [\function_exists('transliterator_transliterate') ? 'g' : '', 'ғ', '-'];
+ yield [\function_exists('transliterator_transliterate') ? 'gh' : '', 'ғ', '-', 'uz'];
+ yield [\function_exists('transliterator_transliterate') ? 'gh' : '', 'ғ', '-', 'uz_fr']; // Ensure we get the parent locale
+ }
+
+ /** @dataProvider provideSlugTests */
+ public function testSlug(string $expected, string $string, string $separator = '-', string $locale = null)
+ {
+ $slugger = new AsciiSlugger();
+
+ $this->assertSame($expected, (string) $slugger->slug($string, $separator, $locale));
+ }
+}
diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php
index d7c11dd990d9e..35220d7385e7e 100644
--- a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php
+++ b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php
@@ -95,8 +95,12 @@ public function write(TranslatorBagInterface $translatorBag): void
}
foreach ($responses as $response) {
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to upload translations to Crowdin: "%s".', $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException('Unable to upload translations to Crowdin.', $response);
+ }
}
}
}
@@ -135,9 +139,13 @@ public function read(array $domains, array $locales): TranslatorBag
continue;
}
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to export file: "%s".', $response->getContent(false)));
+ if (500 <= $statusCode) {
+ throw new ProviderException('Unable to export file.', $response);
+ }
+
continue;
}
@@ -146,9 +154,13 @@ public function read(array $domains, array $locales): TranslatorBag
}
foreach ($downloads as [$response, $locale, $domain]) {
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to download file content: "%s".', $response->getContent(false)));
+ if (500 <= $statusCode) {
+ throw new ProviderException('Unable to download file content.', $response);
+ }
+
continue;
}
@@ -192,8 +204,12 @@ public function delete(TranslatorBagInterface $translatorBag): void
continue;
}
- if (204 !== $response->getStatusCode()) {
+ if (204 !== $statusCode = $response->getStatusCode()) {
$this->logger->warning(sprintf('Unable to delete string: "%s".', $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException('Unable to delete string.', $response);
+ }
}
}
}
@@ -228,8 +244,8 @@ private function addFile(string $domain, string $content): ?array
$storageId = $this->addStorage($domain, $content);
/**
- * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.getMany (Crowdin API)
- * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.files.getMany (Crowdin Enterprise API)
+ * @see https://developer.crowdin.com/api/v2/#operation/api.projects.files.getMany (Crowdin API)
+ * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.files.getMany (Crowdin Enterprise API)
*/
$response = $this->client->request('POST', 'files', [
'json' => [
@@ -238,9 +254,13 @@ private function addFile(string $domain, string $content): ?array
],
]);
- if (201 !== $response->getStatusCode()) {
+ if (201 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to create a File in Crowdin for domain "%s": "%s".', $domain, $response->getContent(false)));
+ if (500 <= $statusCode) {
+ throw new ProviderException(sprintf('Unable to create a File in Crowdin for domain "%s".', $domain), $response);
+ }
+
return null;
}
@@ -252,8 +272,8 @@ private function updateFile(int $fileId, string $domain, string $content): ?arra
$storageId = $this->addStorage($domain, $content);
/**
- * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.put (Crowdin API)
- * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.files.put (Crowdin Enterprise API)
+ * @see https://developer.crowdin.com/api/v2/#operation/api.projects.files.put (Crowdin API)
+ * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.files.put (Crowdin Enterprise API)
*/
$response = $this->client->request('PUT', 'files/'.$fileId, [
'json' => [
@@ -261,9 +281,13 @@ private function updateFile(int $fileId, string $domain, string $content): ?arra
],
]);
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to update file in Crowdin for file ID "%d" and domain "%s": "%s".', $fileId, $domain, $response->getContent(false)));
+ if (500 <= $statusCode) {
+ throw new ProviderException(sprintf('Unable to update file in Crowdin for file ID "%d" and domain "%s".', $fileId, $domain), $response);
+ }
+
return null;
}
@@ -275,8 +299,8 @@ private function uploadTranslations(int $fileId, string $domain, string $content
$storageId = $this->addStorage($domain, $content);
/*
- * @see https://support.crowdin.com/api/v2/#operation/api.projects.translations.postOnLanguage (Crowdin API)
- * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.translations.postOnLanguage (Crowdin Enterprise API)
+ * @see https://developer.crowdin.com/api/v2/#operation/api.projects.translations.postOnLanguage (Crowdin API)
+ * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.postOnLanguage (Crowdin Enterprise API)
*/
return $this->client->request('POST', 'translations/'.str_replace('_', '-', $locale), [
'json' => [
@@ -289,8 +313,8 @@ private function uploadTranslations(int $fileId, string $domain, string $content
private function exportProjectTranslations(string $languageId, int $fileId): ResponseInterface
{
/*
- * @see https://support.crowdin.com/api/v2/#operation/api.projects.translations.exports.post (Crowdin API)
- * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.translations.exports.post (Crowdin Enterprise API)
+ * @see https://developer.crowdin.com/api/v2/#operation/api.projects.translations.exports.post (Crowdin API)
+ * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.exports.post (Crowdin Enterprise API)
*/
return $this->client->request('POST', 'translations/exports', [
'json' => [
@@ -303,8 +327,8 @@ private function exportProjectTranslations(string $languageId, int $fileId): Res
private function downloadSourceFile(int $fileId): ResponseInterface
{
/*
- * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.download.get (Crowdin API)
- * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.files.download.get (Crowdin Enterprise API)
+ * @see https://developer.crowdin.com/api/v2/#operation/api.projects.files.download.get (Crowdin API)
+ * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.files.download.get (Crowdin Enterprise API)
*/
return $this->client->request('GET', sprintf('files/%d/download', $fileId));
}
@@ -312,8 +336,8 @@ private function downloadSourceFile(int $fileId): ResponseInterface
private function listStrings(int $fileId, int $limit, int $offset): array
{
/**
- * @see https://support.crowdin.com/api/v2/#operation/api.projects.strings.getMany (Crowdin API)
- * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.strings.getMany (Crowdin Enterprise API)
+ * @see https://developer.crowdin.com/api/v2/#operation/api.projects.strings.getMany (Crowdin API)
+ * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.strings.getMany (Crowdin Enterprise API)
*/
$response = $this->client->request('GET', 'strings', [
'query' => [
@@ -324,9 +348,7 @@ private function listStrings(int $fileId, int $limit, int $offset): array
]);
if (200 !== $response->getStatusCode()) {
- $this->logger->error(sprintf('Unable to list strings for file %d: "%s".', $fileId, $response->getContent()));
-
- return [];
+ throw new ProviderException(sprintf('Unable to list strings for file "%d".', $fileId), $response);
}
return $response->toArray()['data'];
@@ -335,8 +357,8 @@ private function listStrings(int $fileId, int $limit, int $offset): array
private function deleteString(int $stringId): ResponseInterface
{
/*
- * @see https://support.crowdin.com/api/v2/#operation/api.projects.strings.delete (Crowdin API)
- * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.strings.delete (Crowdin Enterprise API)
+ * @see https://developer.crowdin.com/api/v2/#operation/api.projects.strings.delete (Crowdin API)
+ * @see https://developer.crowdin.com/enterprise/api/v2#operation/api.projects.strings.delete (Crowdin Enterprise API)
*/
return $this->client->request('DELETE', 'strings/'.$stringId);
}
@@ -344,8 +366,8 @@ private function deleteString(int $stringId): ResponseInterface
private function addStorage(string $domain, string $content): int
{
/**
- * @see https://support.crowdin.com/api/v2/#operation/api.storages.post (Crowdin API)
- * @see https://support.crowdin.com/enterprise/api/#operation/api.storages.post (Crowdin Enterprise API)
+ * @see https://developer.crowdin.com/api/v2/#operation/api.storages.post (Crowdin API)
+ * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.storages.post (Crowdin Enterprise API)
*/
$response = $this->client->request('POST', '../../storages', [
'headers' => [
@@ -367,8 +389,8 @@ private function getFileList(): array
$result = [];
/**
- * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.getMany (Crowdin API)
- * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.files.getMany (Crowdin Enterprise API)
+ * @see https://developer.crowdin.com/api/v2/#operation/api.projects.files.getMany (Crowdin API)
+ * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.files.getMany (Crowdin Enterprise API)
*/
$response = $this->client->request('GET', 'files');
diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php b/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php
index c21820829834e..8ecee4d1bfe95 100644
--- a/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php
+++ b/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php
@@ -16,6 +16,7 @@
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProvider;
use Symfony\Component\Translation\Dumper\XliffFileDumper;
+use Symfony\Component\Translation\Exception\ProviderException;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;
@@ -155,6 +156,250 @@ public function testCompleteWriteProcessAddFiles()
$provider->write($translatorBag);
}
+ public function testWriteAddFileServerError()
+ {
+ $this->xliffFileDumper = new XliffFileDumper();
+
+ $expectedMessagesFileContent = <<<'XLIFF'
+
+
+
+
+
+
+ a
+ trans_en_a
+
+
+
+
+
+XLIFF;
+
+ $responses = [
+ 'listFiles' => function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url);
+ $this->assertSame('Authorization: Bearer API_TOKEN', $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse(json_encode(['data' => []]));
+ },
+ 'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
+ $this->assertSame('Content-Type: application/octet-stream', $options['normalized_headers']['content-type'][0]);
+ $this->assertSame('Crowdin-API-FileName: messages.xlf', $options['normalized_headers']['crowdin-api-filename'][0]);
+ $this->assertSame($expectedMessagesFileContent, $options['body']);
+
+ return new MockResponse(json_encode(['data' => ['id' => 19]]), ['http_code' => 201]);
+ },
+ 'addFile' => function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url);
+ $this->assertSame('{"storageId":19,"name":"messages.xlf"}', $options['body']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ 'validators' => ['post.num_comments' => '{count, plural, one {# comment} other {# comments}}'],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/',
+ 'auth_bearer' => 'API_TOKEN',
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2/projects/1/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to create a File in Crowdin for domain "messages".');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteUpdateFileServerError()
+ {
+ $this->xliffFileDumper = new XliffFileDumper();
+
+ $expectedMessagesFileContent = <<<'XLIFF'
+
+
+
+
+
+
+ a
+ trans_en_a
+
+
+
+
+
+XLIFF;
+
+ $responses = [
+ 'listFiles' => function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url);
+ $this->assertSame('Authorization: Bearer API_TOKEN', $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse(json_encode([
+ 'data' => [
+ ['data' => [
+ 'id' => 12,
+ 'name' => 'messages.xlf',
+ ]],
+ ],
+ ]));
+ },
+ 'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
+ $this->assertSame('Content-Type: application/octet-stream', $options['normalized_headers']['content-type'][0]);
+ $this->assertSame('Crowdin-API-FileName: messages.xlf', $options['normalized_headers']['crowdin-api-filename'][0]);
+ $this->assertSame($expectedMessagesFileContent, $options['body']);
+
+ return new MockResponse(json_encode(['data' => ['id' => 19]]), ['http_code' => 201]);
+ },
+ 'UpdateFile' => function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('PUT', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files/12', $url);
+ $this->assertSame('{"storageId":19}', $options['body']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ 'validators' => ['post.num_comments' => '{count, plural, one {# comment} other {# comments}}'],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/',
+ 'auth_bearer' => 'API_TOKEN',
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2/projects/1/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to update file in Crowdin for file ID "12" and domain "messages".');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteUploadTranslationsServerError()
+ {
+ $this->xliffFileDumper = new XliffFileDumper();
+
+ $expectedMessagesTranslationsContent = <<<'XLIFF'
+
+
+
+
+
+
+ a
+ trans_fr_a
+
+
+
+
+
+XLIFF;
+
+ $expectedMessagesFileContent = <<<'XLIFF'
+
+
+
+
+
+
+ a
+ trans_en_a
+
+
+
+
+
+XLIFF;
+
+ $responses = [
+ 'listFiles' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url);
+
+ return new MockResponse(json_encode([
+ 'data' => [
+ ['data' => [
+ 'id' => 12,
+ 'name' => 'messages.xlf',
+ ]],
+ ],
+ ]));
+ },
+ 'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
+ $this->assertSame('Content-Type: application/octet-stream', $options['normalized_headers']['content-type'][0]);
+ $this->assertSame('Crowdin-API-FileName: messages.xlf', $options['normalized_headers']['crowdin-api-filename'][0]);
+ $this->assertSame($expectedMessagesFileContent, $options['body']);
+
+ return new MockResponse(json_encode(['data' => ['id' => 19]]), ['http_code' => 201]);
+ },
+ 'updateFile' => function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('PUT', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files/12', $url);
+ $this->assertSame('{"storageId":19}', $options['body']);
+
+ return new MockResponse(json_encode(['data' => ['id' => 12, 'name' => 'messages.xlf']]));
+ },
+ 'addStorage2' => function (string $method, string $url, array $options = []) use ($expectedMessagesTranslationsContent): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
+ $this->assertSame('Content-Type: application/octet-stream', $options['normalized_headers']['content-type'][0]);
+ $this->assertSame('Crowdin-API-FileName: messages.xlf', $options['normalized_headers']['crowdin-api-filename'][0]);
+ $this->assertSame($expectedMessagesTranslationsContent, $options['body']);
+
+ return new MockResponse(json_encode(['data' => ['id' => 19]]), ['http_code' => 201]);
+ },
+ 'UploadTranslations' => function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame(sprintf('https://api.crowdin.com/api/v2/projects/1/translations/%s', 'fr'), $url);
+ $this->assertSame('{"storageId":19,"fileId":12}', $options['body']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ ]));
+ $translatorBag->addCatalogue(new MessageCatalogue('fr', [
+ 'messages' => ['a' => 'trans_fr_a'],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/',
+ 'auth_bearer' => 'API_TOKEN',
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2/projects/1/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to upload translations to Crowdin.');
+
+ $provider->write($translatorBag);
+ }
+
public function testCompleteWriteProcessUpdateFiles()
{
$this->xliffFileDumper = new XliffFileDumper();
@@ -563,6 +808,82 @@ public function getResponsesForDefaultLocaleAndOneDomain(): \Generator
];
}
+ public function testReadServerException()
+ {
+ $responses = [
+ 'listFiles' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url);
+
+ return new MockResponse(json_encode([
+ 'data' => [
+ ['data' => [
+ 'id' => 12,
+ 'name' => 'messages.xlf',
+ ]],
+ ],
+ ]));
+ },
+ 'exportProjectTranslations' => function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/translations/exports', $url);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $crowdinProvider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/',
+ 'auth_bearer' => 'API_TOKEN',
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to export file.');
+
+ $crowdinProvider->read(['messages'], ['fr']);
+ }
+
+ public function testReadDownloadServerException()
+ {
+ $responses = [
+ 'listFiles' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url);
+
+ return new MockResponse(json_encode([
+ 'data' => [
+ ['data' => [
+ 'id' => 12,
+ 'name' => 'messages.xlf',
+ ]],
+ ],
+ ]));
+ },
+ 'exportProjectTranslations' => function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/translations/exports', $url);
+
+ return new MockResponse(json_encode(['data' => ['url' => 'https://file.url']]));
+ },
+ 'downloadFile' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://file.url/', $url);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $crowdinProvider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/',
+ 'auth_bearer' => 'API_TOKEN',
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to download file content.');
+
+ $crowdinProvider->read(['messages'], ['fr']);
+ }
+
public function testDelete()
{
$responses = [
@@ -631,4 +952,111 @@ public function testDelete()
$provider->delete($translatorBag);
}
+
+ public function testDeleteListStringServerException()
+ {
+ $responses = [
+ 'listFiles' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url);
+
+ return new MockResponse(json_encode([
+ 'data' => [
+ ['data' => [
+ 'id' => 12,
+ 'name' => 'messages.xlf',
+ ]],
+ ],
+ ]));
+ },
+ 'listStrings' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/strings?fileId=12&limit=500&offset=0', $url);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => [
+ 'en a' => 'en a',
+ ],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/',
+ 'auth_bearer' => 'API_TOKEN',
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2/projects/1/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to list strings for file "12".');
+
+ $provider->delete($translatorBag);
+ }
+
+ public function testDeleteDeleteStringServerException()
+ {
+ $responses = [
+ 'listFiles' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url);
+
+ return new MockResponse(json_encode([
+ 'data' => [
+ ['data' => [
+ 'id' => 12,
+ 'name' => 'messages.xlf',
+ ]],
+ ],
+ ]));
+ },
+ 'listStrings' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/strings?fileId=12&limit=500&offset=0', $url);
+
+ return new MockResponse(json_encode([
+ 'data' => [
+ ['data' => ['id' => 1, 'text' => 'en a']],
+ ['data' => ['id' => 2, 'text' => 'en b']],
+ ],
+ ]));
+ },
+ 'listStrings2' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/strings?fileId=12&limit=500&offset=500', $url);
+
+ $response = $this->createMock(ResponseInterface::class);
+ $response->expects($this->any())
+ ->method('getContent')
+ ->with(false)
+ ->willReturn(json_encode(['data' => []]));
+
+ return $response;
+ },
+ 'deleteString1' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('DELETE', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/strings/1', $url);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => [
+ 'en a' => 'en a',
+ ],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/',
+ 'auth_bearer' => 'API_TOKEN',
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2/projects/1/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to delete string.');
+
+ $provider->delete($translatorBag);
+ }
}
diff --git a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php
index ca2dad34168f1..466584a3363a7 100644
--- a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php
+++ b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php
@@ -123,8 +123,12 @@ public function read(array $domains, array $locales): TranslatorBag
$this->logger->info(sprintf('No modifications found for locale "%s" and domain "%s" in Loco.', $locale, $domain));
$catalogue = new MessageCatalogue($locale);
+ $previousMessages = $previousCatalogue->all($domain);
- foreach ($previousCatalogue->all($domain) as $key => $message) {
+ if (!str_ends_with($domain, $catalogue::INTL_DOMAIN_SUFFIX)) {
+ $previousMessages = array_diff_key($previousMessages, $previousCatalogue->all($domain.$catalogue::INTL_DOMAIN_SUFFIX));
+ }
+ foreach ($previousMessages as $key => $message) {
$catalogue->set($this->retrieveKeyFromId($key, $domain), $message, $domain);
}
@@ -184,12 +188,16 @@ public function delete(TranslatorBagInterface $translatorBag): void
}
foreach ($responses as $key => $response) {
- if (403 === $response->getStatusCode()) {
+ if (403 === $statusCode = $response->getStatusCode()) {
$this->logger->error('The API key used does not have sufficient permissions to delete assets.');
}
- if (200 !== $response->getStatusCode() && 404 !== $response->getStatusCode()) {
+ if (200 !== $statusCode && 404 !== $statusCode) {
$this->logger->error(sprintf('Unable to delete translation key "%s" to Loco: "%s".', $key, $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException(sprintf('Unable to delete translation key "%s" to Loco.', $key), $response);
+ }
}
}
}
@@ -201,8 +209,12 @@ private function getAssetsIds(string $domain): array
{
$response = $this->client->request('GET', 'assets', ['query' => ['filter' => $domain]]);
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to get assets from Loco: "%s".', $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException('Unable to get assets from Loco.', $response);
+ }
}
return array_map(function ($asset) {
@@ -226,8 +238,12 @@ private function createAssets(array $keys, string $domain): array
}
foreach ($responses as $key => $response) {
- if (201 !== $response->getStatusCode()) {
- $this->logger->error(sprintf('Unable to add new translation key "%s" to Loco: (status code: "%s") "%s".', $key, $response->getStatusCode(), $response->getContent(false)));
+ if (201 !== $statusCode = $response->getStatusCode()) {
+ $this->logger->error(sprintf('Unable to add new translation key "%s" to Loco: (status code: "%s") "%s".', $key, $statusCode, $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException(sprintf('Unable to add new translation key "%s" to Loco: (status code: "%s").', $key, $statusCode), $response);
+ }
} else {
$createdIds[] = $response->toArray(false)['id'];
}
@@ -248,8 +264,12 @@ private function translateAssets(array $translations, string $locale): void
}
foreach ($responses as $id => $response) {
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to add translation for key "%s" in locale "%s" to Loco: "%s".', $id, $locale, $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException(sprintf('Unable to add translation for key "%s" in locale "%s" to Loco.', $id, $locale), $response);
+ }
}
}
}
@@ -270,13 +290,19 @@ private function tagsAssets(array $ids, string $tag): void
}
}
- // Set tags for all ids without comma.
- $response = $this->client->request('POST', sprintf('tags/%s.json', rawurlencode($tag)), [
- 'body' => implode(',', $idsWithoutComma),
- ]);
+ if ([] !== $idsWithoutComma) {
+ // Set tags for all ids without comma.
+ $response = $this->client->request('POST', sprintf('tags/%s.json', rawurlencode($tag)), [
+ 'body' => implode(',', $idsWithoutComma),
+ ]);
- if (200 !== $response->getStatusCode()) {
- $this->logger->error(sprintf('Unable to tag assets with "%s" on Loco: "%s".', $tag, $response->getContent(false)));
+ if (200 !== $statusCode = $response->getStatusCode()) {
+ $this->logger->error(sprintf('Unable to tag assets with "%s" on Loco: "%s".', $tag, $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException(sprintf('Unable to tag assets with "%s" on Loco.', $tag), $response);
+ }
+ }
}
// Set tags for each id with comma one by one.
@@ -285,8 +311,12 @@ private function tagsAssets(array $ids, string $tag): void
'body' => ['name' => $tag],
]);
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to tag asset "%s" with "%s" on Loco: "%s".', $id, $tag, $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException(sprintf('Unable to tag asset "%s" with "%s" on Loco.', $id, $tag), $response);
+ }
}
}
}
@@ -299,8 +329,12 @@ private function createTag(string $tag): void
],
]);
- if (201 !== $response->getStatusCode()) {
+ if (201 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to create tag "%s" on Loco: "%s".', $tag, $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException(sprintf('Unable to create tag "%s" on Loco.', $tag), $response);
+ }
}
}
@@ -324,8 +358,12 @@ private function createLocale(string $locale): void
],
]);
- if (201 !== $response->getStatusCode()) {
+ if (201 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to create locale "%s" on Loco: "%s".', $locale, $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException(sprintf('Unable to create locale "%s" on Loco.', $locale), $response);
+ }
}
}
diff --git a/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php b/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php
index af6342d339d8a..5f4ab42b346f5 100644
--- a/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php
+++ b/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php
@@ -15,6 +15,7 @@
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\Translation\Bridge\Loco\LocoProvider;
+use Symfony\Component\Translation\Exception\ProviderException;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\Loader\XliffFileLoader;
@@ -250,6 +251,451 @@ public function testCompleteWriteProcess()
$provider->write($translatorBag);
}
+ public function testWriteCreateAssetServerError()
+ {
+ $expectedAuthHeader = 'Authorization: Loco API_KEY';
+
+ $responses = [
+ 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $expectedBody = http_build_query([
+ 'id' => 'messages__a',
+ 'text' => 'a',
+ 'type' => 'text',
+ 'default' => 'untranslated',
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame($expectedBody, $options['body']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://localise.biz/api/',
+ 'headers' => ['Authorization' => 'Loco API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to add new translation key "a" to Loco: (status code: "500").');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteCreateTagServerError()
+ {
+ $expectedAuthHeader = 'Authorization: Loco API_KEY';
+
+ $responses = [
+ 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $expectedBody = http_build_query([
+ 'id' => 'messages__a',
+ 'text' => 'a',
+ 'type' => 'text',
+ 'default' => 'untranslated',
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame($expectedBody, $options['body']);
+
+ return new MockResponse('{"id": "messages__a"}', ['http_code' => 201]);
+ },
+ 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('[]');
+ },
+ 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame(http_build_query(['name' => 'messages']), $options['body']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://localise.biz/api/',
+ 'headers' => ['Authorization' => 'Loco API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to create tag "messages" on Loco.');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteTagAssetsServerError()
+ {
+ $expectedAuthHeader = 'Authorization: Loco API_KEY';
+
+ $responses = [
+ 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $expectedBody = http_build_query([
+ 'id' => 'messages__a',
+ 'text' => 'a',
+ 'type' => 'text',
+ 'default' => 'untranslated',
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame($expectedBody, $options['body']);
+
+ return new MockResponse('{"id": "messages__a"}', ['http_code' => 201]);
+ },
+ 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('[]');
+ },
+ 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame(http_build_query(['name' => 'messages']), $options['body']);
+
+ return new MockResponse('', ['http_code' => 201]);
+ },
+ 'tagAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/tags/messages.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame('messages__a', $options['body']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://localise.biz/api/',
+ 'headers' => ['Authorization' => 'Loco API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to tag assets with "messages" on Loco.');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteTagAssetsServerErrorWithComma()
+ {
+ $expectedAuthHeader = 'Authorization: Loco API_KEY';
+
+ $responses = [
+ 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $expectedBody = http_build_query([
+ 'id' => 'messages__a',
+ 'text' => 'a',
+ 'type' => 'text',
+ 'default' => 'untranslated',
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame($expectedBody, $options['body']);
+
+ return new MockResponse('{"id": "messages__a,messages__b"}', ['http_code' => 201]);
+ },
+ 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('[]');
+ },
+ 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame(http_build_query(['name' => 'messages']), $options['body']);
+
+ return new MockResponse('', ['http_code' => 201]);
+ },
+ 'tagAssetWithComma' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/assets/messages__a%2Cmessages__b/tags', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame('name=messages', $options['body']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://localise.biz/api/',
+ 'headers' => ['Authorization' => 'Loco API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to tag asset "messages__a,messages__b" with "messages" on Loco.');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteCreateLocaleServerError()
+ {
+ $expectedAuthHeader = 'Authorization: Loco API_KEY';
+
+ $responses = [
+ 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $expectedBody = http_build_query([
+ 'id' => 'messages__a',
+ 'text' => 'a',
+ 'type' => 'text',
+ 'default' => 'untranslated',
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame($expectedBody, $options['body']);
+
+ return new MockResponse('{"id": "messages__a"}', ['http_code' => 201]);
+ },
+ 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('[]');
+ },
+ 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame(http_build_query(['name' => 'messages']), $options['body']);
+
+ return new MockResponse('', ['http_code' => 201]);
+ },
+ 'tagAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/tags/messages.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame('messages__a', $options['body']);
+
+ return new MockResponse();
+ },
+ 'getLocales' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/locales', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('[{"code":"fr"}]');
+ },
+ 'createLocale' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/locales', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://localise.biz/api/',
+ 'headers' => ['Authorization' => 'Loco API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to create locale "en" on Loco.');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteGetAssetsIdsServerError()
+ {
+ $expectedAuthHeader = 'Authorization: Loco API_KEY';
+
+ $responses = [
+ 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $expectedBody = http_build_query([
+ 'id' => 'messages__a',
+ 'text' => 'a',
+ 'type' => 'text',
+ 'default' => 'untranslated',
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame($expectedBody, $options['body']);
+
+ return new MockResponse('{"id": "messages__a"}', ['http_code' => 201]);
+ },
+ 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('[]');
+ },
+ 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame(http_build_query(['name' => 'messages']), $options['body']);
+
+ return new MockResponse('', ['http_code' => 201]);
+ },
+ 'tagAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/tags/messages.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame('messages__a', $options['body']);
+
+ return new MockResponse();
+ },
+ 'getLocales' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/locales', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('[{"code":"en"}]');
+ },
+ 'getAssetsIds' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/assets?filter=messages', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ ]));
+ $translatorBag->addCatalogue(new MessageCatalogue('fr', [
+ 'messages' => ['a' => 'trans_fr_a'],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://localise.biz/api/',
+ 'headers' => ['Authorization' => 'Loco API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to get assets from Loco.');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteTranslateAssetsServerError()
+ {
+ $expectedAuthHeader = 'Authorization: Loco API_KEY';
+
+ $responses = [
+ 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $expectedBody = http_build_query([
+ 'id' => 'messages__a',
+ 'text' => 'a',
+ 'type' => 'text',
+ 'default' => 'untranslated',
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame($expectedBody, $options['body']);
+
+ return new MockResponse('{"id": "messages__a"}', ['http_code' => 201]);
+ },
+ 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('[]');
+ },
+ 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/tags.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame(http_build_query(['name' => 'messages']), $options['body']);
+
+ return new MockResponse('', ['http_code' => 201]);
+ },
+ 'tagAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/tags/messages.json', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame('messages__a', $options['body']);
+
+ return new MockResponse();
+ },
+ 'getLocales' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/locales', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('[{"code":"en"}]');
+ },
+ 'getAssetsIds' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/assets?filter=messages', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+
+ return new MockResponse('[{"id":"messages__foo.existing_key"},{"id":"messages__a"}]');
+ },
+ 'translateAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://localise.biz/api/translations/messages__a/en', $url);
+ $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]);
+ $this->assertSame('trans_en_a', $options['body']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ];
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ ]));
+ $translatorBag->addCatalogue(new MessageCatalogue('fr', [
+ 'messages' => ['a' => 'trans_fr_a'],
+ ]));
+
+ $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([
+ 'base_uri' => 'https://localise.biz/api/',
+ 'headers' => ['Authorization' => 'Loco API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/');
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to add translation for key "messages__a" in locale "en" to Loco.');
+
+ $provider->write($translatorBag);
+ }
+
/**
* @dataProvider getResponsesForOneLocaleAndOneDomain
*/
@@ -450,6 +896,41 @@ function (string $method, string $url): MockResponse {
$provider->delete($translatorBag);
}
+ public function testDeleteServerError()
+ {
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['a' => 'trans_en_a'],
+ ]));
+
+ $provider = $this->createProvider(
+ new MockHttpClient([
+ function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://localise.biz/api/assets?filter=messages', $url);
+ $this->assertSame(['filter' => 'messages'], $options['query']);
+
+ return new MockResponse('[{"id":"messages__a"}]');
+ },
+ function (string $method, string $url): MockResponse {
+ $this->assertSame('DELETE', $method);
+ $this->assertSame('https://localise.biz/api/assets/messages__a.json', $url);
+
+ return new MockResponse('', ['http_code' => 500]);
+ },
+ ], 'https://localise.biz/api/'),
+ $this->getLoader(),
+ $this->getLogger(),
+ $this->getDefaultLocale(),
+ 'localise.biz/api/'
+ );
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to delete translation key "messages__a" to Loco.');
+
+ $provider->delete($translatorBag);
+ }
+
public function getResponsesForOneLocaleAndOneDomain(): \Generator
{
$arrayLoader = new ArrayLoader();
@@ -458,9 +939,9 @@ public function getResponsesForOneLocaleAndOneDomain(): \Generator
$expectedTranslatorBagEn->addCatalogue($arrayLoader->load([
'index.hello' => 'Hello',
'index.greetings' => 'Welcome, {firstname}!',
- ], 'en'));
+ ], 'en', 'messages+intl-icu'));
- yield ['en', 'messages', <<<'XLIFF'
+ yield ['en', 'messages+intl-icu', <<<'XLIFF'
@@ -468,7 +949,7 @@ public function getResponsesForOneLocaleAndOneDomain(): \Generator
-
+
index.hello
Hello
@@ -488,9 +969,9 @@ public function getResponsesForOneLocaleAndOneDomain(): \Generator
$expectedTranslatorBagFr->addCatalogue($arrayLoader->load([
'index.hello' => 'Bonjour',
'index.greetings' => 'Bienvenue, {firstname} !',
- ], 'fr'));
+ ], 'fr', 'messages+intl-icu'));
- yield ['fr', 'messages', <<<'XLIFF'
+ yield ['fr', 'messages+intl-icu', <<<'XLIFF'
@@ -498,7 +979,7 @@ public function getResponsesForOneLocaleAndOneDomain(): \Generator
-
+
index.hello
Bonjour
diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php
index 167430f1848d8..c28b42b289c68 100644
--- a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php
+++ b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php
@@ -120,10 +120,12 @@ public function delete(TranslatorBagInterface $translatorBag): void
$keysIds = [];
foreach ($catalogue->getDomains() as $domain) {
- $keysToDelete = [];
- foreach (array_keys($catalogue->all($domain)) as $key) {
- $keysToDelete[] = $key;
+ $keysToDelete = array_keys($catalogue->all($domain));
+
+ if (!$keysToDelete) {
+ continue;
}
+
$keysIds += $this->getKeysIds($keysToDelete, $domain);
}
@@ -198,9 +200,13 @@ private function createKeys(array $keys, string $domain): array
$createdKeys = [];
foreach ($responses as $response) {
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to create keys to Lokalise: "%s".', $response->getContent(false)));
+ if (500 <= $statusCode) {
+ throw new ProviderException('Unable to create keys to Lokalise.', $response);
+ }
+
continue;
}
@@ -254,8 +260,12 @@ private function updateTranslations(array $keysByDomain, TranslatorBagInterface
'json' => ['keys' => $keysToUpdate],
]);
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to create/update translations to Lokalise: "%s".', $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException('Unable to create/update translations to Lokalise.', $response);
+ }
}
}
@@ -270,8 +280,12 @@ private function getKeysIds(array $keys, string $domain, int $page = 1): array
],
]);
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to get keys ids from Lokalise: "%s".', $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException('Unable to get keys ids from Lokalise.', $response);
+ }
}
$result = [];
@@ -320,9 +334,13 @@ private function getLanguages(): array
{
$response = $this->client->request('GET', 'languages');
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to get languages from Lokalise: "%s".', $response->getContent(false)));
+ if (500 <= $statusCode) {
+ throw new ProviderException('Unable to get languages from Lokalise.', $response);
+ }
+
return [];
}
@@ -345,8 +363,12 @@ private function createLanguages(array $languages): void
],
]);
- if (200 !== $response->getStatusCode()) {
+ if (200 !== $statusCode = $response->getStatusCode()) {
$this->logger->error(sprintf('Unable to create languages on Lokalise: "%s".', $response->getContent(false)));
+
+ if (500 <= $statusCode) {
+ throw new ProviderException('Unable to create languages on Lokalise.', $response);
+ }
}
}
diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php b/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php
index 7ce9b8e067ada..5df996e94327b 100644
--- a/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php
+++ b/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php
@@ -15,6 +15,7 @@
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProvider;
+use Symfony\Component\Translation\Exception\ProviderException;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\Loader\XliffFileLoader;
@@ -247,6 +248,307 @@ public function testCompleteWriteProcess()
$this->assertTrue($updateProcessed, 'Translations update was not called.');
}
+ public function testWriteGetLanguageServerError()
+ {
+ $getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
+
+ return new MockResponse('', ['http_code' => 500]);
+ };
+
+ $provider = $this->createProvider((new MockHttpClient([
+ $getLanguagesResponse,
+ ]))->withOptions([
+ 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/',
+ 'headers' => ['X-Api-Token' => 'API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com');
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['young_dog' => 'puppy'],
+ ]));
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to get languages from Lokalise.');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteCreateLanguageServerError()
+ {
+ $getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
+
+ return new MockResponse(json_encode(['languages' => []]));
+ };
+
+ $createLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $expectedBody = json_encode([
+ 'languages' => [
+ ['lang_iso' => 'en'],
+ ],
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
+ $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ };
+
+ $provider = $this->createProvider((new MockHttpClient([
+ $getLanguagesResponse,
+ $createLanguagesResponse,
+ ]))->withOptions([
+ 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/',
+ 'headers' => ['X-Api-Token' => 'API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com');
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['young_dog' => 'puppy'],
+ ]));
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to create languages on Lokalise.');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteGetKeysIdsServerError()
+ {
+ $getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
+
+ return new MockResponse(json_encode(['languages' => []]));
+ };
+
+ $createLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $expectedBody = json_encode([
+ 'languages' => [
+ ['lang_iso' => 'en'],
+ ],
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
+ $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']);
+
+ return new MockResponse(json_encode(['keys' => []]));
+ };
+
+ $getKeysIdsForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $expectedQuery = [
+ 'filter_keys' => '',
+ 'filter_filenames' => 'messages.xliff',
+ 'limit' => 5000,
+ 'page' => 1,
+ ];
+
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys?'.http_build_query($expectedQuery), $url);
+ $this->assertSame($expectedQuery, $options['query']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ };
+
+ $provider = $this->createProvider((new MockHttpClient([
+ $getLanguagesResponse,
+ $createLanguagesResponse,
+ $getKeysIdsForMessagesDomainResponse,
+ ]))->withOptions([
+ 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/',
+ 'headers' => ['X-Api-Token' => 'API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com');
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['young_dog' => 'puppy'],
+ ]));
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to get keys ids from Lokalise.');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteCreateKeysServerError()
+ {
+ $getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
+
+ return new MockResponse(json_encode(['languages' => []]));
+ };
+
+ $createLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $expectedBody = json_encode([
+ 'languages' => [
+ ['lang_iso' => 'en'],
+ ],
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
+ $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']);
+
+ return new MockResponse(json_encode(['keys' => []]));
+ };
+
+ $getKeysIdsForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $expectedQuery = [
+ 'filter_keys' => '',
+ 'filter_filenames' => 'messages.xliff',
+ 'limit' => 5000,
+ 'page' => 1,
+ ];
+
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys?'.http_build_query($expectedQuery), $url);
+ $this->assertSame($expectedQuery, $options['query']);
+
+ return new MockResponse(json_encode(['keys' => []]));
+ };
+
+ $createKeysForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $expectedBody = json_encode([
+ 'keys' => [
+ [
+ 'key_name' => 'young_dog',
+ 'platforms' => ['web'],
+ 'filenames' => [
+ 'web' => 'messages.xliff',
+ 'ios' => null,
+ 'android' => null,
+ 'other' => null,
+ ],
+ ],
+ ],
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']);
+
+ return new MockResponse('', ['http_code' => 500]);
+ };
+
+ $provider = $this->createProvider((new MockHttpClient([
+ $getLanguagesResponse,
+ $createLanguagesResponse,
+ $getKeysIdsForMessagesDomainResponse,
+ $createKeysForMessagesDomainResponse,
+ ]))->withOptions([
+ 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/',
+ 'headers' => ['X-Api-Token' => 'API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com');
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['young_dog' => 'puppy'],
+ ]));
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to create keys to Lokalise.');
+
+ $provider->write($translatorBag);
+ }
+
+ public function testWriteUploadTranslationsServerError()
+ {
+ $getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
+
+ return new MockResponse(json_encode(['languages' => []]));
+ };
+
+ $createLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $expectedBody = json_encode([
+ 'languages' => [
+ ['lang_iso' => 'en'],
+ ],
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
+ $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']);
+
+ return new MockResponse(json_encode(['keys' => []]));
+ };
+
+ $getKeysIdsForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $expectedQuery = [
+ 'filter_keys' => '',
+ 'filter_filenames' => 'messages.xliff',
+ 'limit' => 5000,
+ 'page' => 1,
+ ];
+
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys?'.http_build_query($expectedQuery), $url);
+ $this->assertSame($expectedQuery, $options['query']);
+
+ return new MockResponse(json_encode(['keys' => []]));
+ };
+
+ $createKeysForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
+ $expectedBody = json_encode([
+ 'keys' => [
+ [
+ 'key_name' => 'young_dog',
+ 'platforms' => ['web'],
+ 'filenames' => [
+ 'web' => 'messages.xliff',
+ 'ios' => null,
+ 'android' => null,
+ 'other' => null,
+ ],
+ ],
+ ],
+ ]);
+
+ $this->assertSame('POST', $method);
+ $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']);
+
+ return new MockResponse(json_encode(['keys' => [
+ [
+ 'key_name' => ['web' => 'young_dog'],
+ 'key_id' => 29,
+ ],
+ ]]));
+ };
+
+ $updateTranslationsResponse = function (string $method, string $url, array $options = []) use (&$updateProcessed): ResponseInterface {
+ $this->assertSame('PUT', $method);
+
+ return new MockResponse('', ['http_code' => 500]);
+ };
+
+ $provider = $this->createProvider((new MockHttpClient([
+ $getLanguagesResponse,
+ $createLanguagesResponse,
+ $getKeysIdsForMessagesDomainResponse,
+ $createKeysForMessagesDomainResponse,
+ $updateTranslationsResponse,
+ ]))->withOptions([
+ 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/',
+ 'headers' => ['X-Api-Token' => 'API_KEY'],
+ ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com');
+
+ $translatorBag = new TranslatorBag();
+ $translatorBag->addCatalogue(new MessageCatalogue('en', [
+ 'messages' => ['young_dog' => 'puppy'],
+ ]));
+
+ $this->expectException(ProviderException::class);
+ $this->expectExceptionMessage('Unable to create/update translations to Lokalise.');
+
+ $provider->write($translatorBag);
+ }
+
/**
* @dataProvider getResponsesForOneLocaleAndOneDomain
*/
@@ -397,10 +699,12 @@ public function testDeleteProcess()
$translatorBag->addCatalogue(new MessageCatalogue('en', [
'messages' => ['a' => 'trans_en_a'],
'validators' => ['post.num_comments' => '{count, plural, one {# comment} other {# comments}}'],
+ 'domain_without_missing_messages' => [],
]));
$translatorBag->addCatalogue(new MessageCatalogue('fr', [
'messages' => ['a' => 'trans_fr_a'],
'validators' => ['post.num_comments' => '{count, plural, one {# commentaire} other {# commentaires}}'],
+ 'domain_without_missing_messages' => [],
]));
$provider = $this->createProvider(
diff --git a/src/Symfony/Component/Translation/MessageCatalogue.php b/src/Symfony/Component/Translation/MessageCatalogue.php
index 09153d12c4b44..4b8fc58d51343 100644
--- a/src/Symfony/Component/Translation/MessageCatalogue.php
+++ b/src/Symfony/Component/Translation/MessageCatalogue.php
@@ -156,19 +156,14 @@ public function replace(array $messages, string $domain = 'messages')
*/
public function add(array $messages, string $domain = 'messages')
{
- if (!isset($this->messages[$domain])) {
- $this->messages[$domain] = [];
- }
- $intlDomain = $domain;
- if (!str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) {
- $intlDomain .= self::INTL_DOMAIN_SUFFIX;
- }
+ $altDomain = str_ends_with($domain, self::INTL_DOMAIN_SUFFIX) ? substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)) : $domain.self::INTL_DOMAIN_SUFFIX;
foreach ($messages as $id => $message) {
- if (isset($this->messages[$intlDomain]) && \array_key_exists($id, $this->messages[$intlDomain])) {
- $this->messages[$intlDomain][$id] = $message;
- } else {
- $this->messages[$domain][$id] = $message;
- }
+ unset($this->messages[$altDomain][$id]);
+ $this->messages[$domain][$id] = $message;
+ }
+
+ if ([] === ($this->messages[$altDomain] ?? null)) {
+ unset($this->messages[$altDomain]);
}
}
diff --git a/src/Symfony/Component/Translation/Tests/Catalogue/MergeOperationTest.php b/src/Symfony/Component/Translation/Tests/Catalogue/MergeOperationTest.php
index 3f21abac9dd52..1c9bcbc265571 100644
--- a/src/Symfony/Component/Translation/Tests/Catalogue/MergeOperationTest.php
+++ b/src/Symfony/Component/Translation/Tests/Catalogue/MergeOperationTest.php
@@ -57,7 +57,7 @@ public function testGetResultFromIntlDomain()
{
$this->assertEquals(
new MessageCatalogue('en', [
- 'messages' => ['a' => 'old_a', 'b' => 'old_b'],
+ 'messages' => ['b' => 'old_b'],
'messages+intl-icu' => ['d' => 'old_d', 'c' => 'new_c', 'a' => 'new_a'],
]),
$this->createOperation(
diff --git a/src/Symfony/Component/Translation/Tests/Catalogue/TargetOperationTest.php b/src/Symfony/Component/Translation/Tests/Catalogue/TargetOperationTest.php
index 2b63cd4166464..6f4de858870dc 100644
--- a/src/Symfony/Component/Translation/Tests/Catalogue/TargetOperationTest.php
+++ b/src/Symfony/Component/Translation/Tests/Catalogue/TargetOperationTest.php
@@ -71,7 +71,6 @@ public function testGetResultWithMixedDomains()
{
$this->assertEquals(
new MessageCatalogue('en', [
- 'messages' => ['a' => 'old_a'],
'messages+intl-icu' => ['a' => 'new_a'],
]),
$this->createOperation(
@@ -103,7 +102,6 @@ public function testGetResultWithMixedDomains()
$this->assertEquals(
new MessageCatalogue('en', [
- 'messages' => ['a' => 'old_a'],
'messages+intl-icu' => ['b' => 'new_b', 'a' => 'new_a'],
]),
$this->createOperation(
diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php
index 57f5456d950cd..b5c332dffd8ac 100644
--- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php
+++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php
@@ -15,6 +15,10 @@
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Translation\Exception\RuntimeException;
+use Symfony\Component\Translation\Formatter\IntlFormatter;
+use Symfony\Component\Translation\Formatter\IntlFormatterInterface;
+use Symfony\Component\Translation\Formatter\MessageFormatter;
+use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\TranslatableMessage;
@@ -563,6 +567,26 @@ public function testIntlFormattedDomain()
$this->assertSame('Hi Bob', $translator->trans('some_message', ['%name%' => 'Bob']));
}
+ public function testIntlDomainOverlapseWithIntlResourceBefore()
+ {
+ $intlFormatterMock = $this->createMock(IntlFormatterInterface::class);
+ $intlFormatterMock->expects($this->once())->method('formatIntl')->with('hello intl', 'en', [])->willReturn('hello intl');
+
+ $messageFormatter = new MessageFormatter(null, $intlFormatterMock);
+
+ $translator = new Translator('en', $messageFormatter);
+ $translator->addLoader('array', new ArrayLoader());
+
+ $translator->addResource('array', ['some_message' => 'hello intl'], 'en', 'messages+intl-icu');
+ $translator->addResource('array', ['some_message' => 'hello'], 'en', 'messages');
+
+ $this->assertSame('hello', $translator->trans('some_message', [], 'messages'));
+
+ $translator->addResource('array', ['some_message' => 'hello intl'], 'en', 'messages+intl-icu');
+
+ $this->assertSame('hello intl', $translator->trans('some_message', [], 'messages'));
+ }
+
public function testMissingLoaderForResourceError()
{
$this->expectException(RuntimeException::class);
diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md
index 8e06d5873506a..0116a138322aa 100644
--- a/src/Symfony/Component/Validator/CHANGELOG.md
+++ b/src/Symfony/Component/Validator/CHANGELOG.md
@@ -70,6 +70,7 @@ CHANGELOG
5.1.0
-----
+ * Add `AtLeastOneOf` constraint that is considered to be valid if at least one of the nested constraints is valid
* added the `Hostname` constraint and validator
* added the `alpha3` option to the `Country` and `Language` constraints
* allow to define a reusable set of constraints by extending the `Compound` constraint
diff --git a/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php b/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php
index 8189219655017..9ef05753cc0a5 100644
--- a/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php
@@ -34,6 +34,10 @@ public function validate(mixed $value, Constraint $constraint)
$messages = [$constraint->message];
foreach ($constraint->constraints as $key => $item) {
+ if (!\in_array($this->context->getGroup(), $item->groups, true)) {
+ continue;
+ }
+
$executionContext = clone $this->context;
$executionContext->setNode($value, $this->context->getObject(), $this->context->getMetadata(), $this->context->getPropertyPath());
$violations = $validator->inContext($executionContext)->validate($value, $item, $this->context->getGroup())->getViolations();
diff --git a/src/Symfony/Component/Validator/Constraints/EmailValidator.php b/src/Symfony/Component/Validator/Constraints/EmailValidator.php
index 3cc4d4e69edbe..744f619fa7762 100644
--- a/src/Symfony/Component/Validator/Constraints/EmailValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/EmailValidator.php
@@ -70,6 +70,10 @@ public function validate(mixed $value, Constraint $constraint)
}
if (null === $constraint->mode) {
+ if (Email::VALIDATION_MODE_STRICT === $this->defaultMode && !class_exists(EguliasEmailValidator::class)) {
+ throw new LogicException(sprintf('The "egulias/email-validator" component is required to make the "%s" constraint default to strict mode.', EguliasEmailValidator::class));
+ }
+
$constraint->mode = $this->defaultMode;
}
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php
index 6be6a5d6f702c..0fb735a84cdb2 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php
@@ -19,6 +19,7 @@
use Symfony\Component\Validator\Constraints\DivisibleBy;
use Symfony\Component\Validator\Constraints\EqualTo;
use Symfony\Component\Validator\Constraints\Expression;
+use Symfony\Component\Validator\Constraints\GreaterThan;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
use Symfony\Component\Validator\Constraints\IdenticalTo;
use Symfony\Component\Validator\Constraints\Language;
@@ -235,6 +236,28 @@ public function hasMetadataFor($classOrObject): bool
$this->assertSame('custom message foo', $violations->get(0)->getMessage());
$this->assertSame('This value should satisfy at least one of the following constraints: [1] custom message bar', $violations->get(1)->getMessage());
}
+
+ public function testNestedConstraintsAreNotExecutedWhenGroupDoesNotMatch()
+ {
+ $validator = Validation::createValidator();
+
+ $violations = $validator->validate(50, new AtLeastOneOf([
+ 'constraints' => [
+ new Range([
+ 'groups' => 'adult',
+ 'min' => 18,
+ 'max' => 55,
+ ]),
+ new GreaterThan([
+ 'groups' => 'senior',
+ 'value' => 55,
+ ]),
+ ],
+ 'groups' => ['adult', 'senior'],
+ ]), 'senior');
+
+ $this->assertCount(1, $violations);
+ }
}
class ExpressionConstraintNested
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SequentiallyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SequentiallyValidatorTest.php
index be11448ad28e4..1dca3ccd1c186 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/SequentiallyValidatorTest.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/SequentiallyValidatorTest.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Tests\Constraints;
+use Symfony\Component\Validator\Constraints\GreaterThan;
use Symfony\Component\Validator\Constraints\NotEqualTo;
use Symfony\Component\Validator\Constraints\Range;
use Symfony\Component\Validator\Constraints\Regex;
@@ -19,6 +20,7 @@
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
+use Symfony\Component\Validator\Validation;
class SequentiallyValidatorTest extends ConstraintValidatorTestCase
{
@@ -61,4 +63,26 @@ public function testStopsAtFirstConstraintWithViolations()
$this->assertCount(1, $this->context->getViolations());
}
+
+ public function testNestedConstraintsAreNotExecutedWhenGroupDoesNotMatch()
+ {
+ $validator = Validation::createValidator();
+
+ $violations = $validator->validate(50, new Sequentially([
+ 'constraints' => [
+ new GreaterThan([
+ 'groups' => 'senior',
+ 'value' => 55,
+ ]),
+ new Range([
+ 'groups' => 'adult',
+ 'min' => 18,
+ 'max' => 55,
+ ]),
+ ],
+ 'groups' => ['adult', 'senior'],
+ ]), 'adult');
+
+ $this->assertCount(0, $violations);
+ }
}
diff --git a/src/Symfony/Component/Yaml/Tests/DumperTest.php b/src/Symfony/Component/Yaml/Tests/DumperTest.php
index 693db4ee0a684..5ac9b81661e65 100644
--- a/src/Symfony/Component/Yaml/Tests/DumperTest.php
+++ b/src/Symfony/Component/Yaml/Tests/DumperTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Yaml\Dumper;
use Symfony\Component\Yaml\Exception\DumpException;
+use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Tag\TaggedValue;
use Symfony\Component\Yaml\Yaml;
@@ -78,7 +79,8 @@ public function testIndentationInConstructor()
- foo
EOF;
- $this->assertEquals($expected, $dumper->dump($this->array, 4, 0));
+ $this->assertSame($expected, $dumper->dump($this->array, 4, 0));
+ $this->assertSameData($this->array, $this->parser->parse($expected));
}
public function testSpecifications()
@@ -94,14 +96,17 @@ public function testSpecifications()
}
$test = $this->parser->parse($yaml);
- if (isset($test['dump_skip']) && $test['dump_skip']) {
+ if ($test['dump_skip'] ?? false) {
continue;
- } elseif (isset($test['todo']) && $test['todo']) {
+ }
+
+ if ($test['todo'] ?? false) {
// TODO
- } else {
- eval('$expected = '.trim($test['php']).';');
- $this->assertSame($expected, $this->parser->parse($this->dumper->dump($expected, 10)), $test['test']);
+ continue;
}
+
+ $expected = eval('return '.trim($test['php']).';');
+ $this->assertSame($expected, $this->parser->parse($this->dumper->dump($expected, 10)), $test['test']);
}
}
}
@@ -111,8 +116,9 @@ public function testInlineLevel()
$expected = <<<'EOF'
{ '': bar, foo: '#bar', "foo'bar": { }, bar: [1, foo, { a: A }], foobar: { foo: bar, bar: [1, foo], foobar: { foo: bar, bar: [1, foo] } } }
EOF;
- $this->assertEquals($expected, $this->dumper->dump($this->array, -10), '->dump() takes an inline level argument');
- $this->assertEquals($expected, $this->dumper->dump($this->array, 0), '->dump() takes an inline level argument');
+ $this->assertSame($expected, $this->dumper->dump($this->array, -10), '->dump() takes an inline level argument');
+ $this->assertSame($expected, $this->dumper->dump($this->array, 0), '->dump() takes an inline level argument');
+ $this->assertSameData($this->array, $this->parser->parse($expected));
$expected = <<<'EOF'
'': bar
@@ -122,7 +128,8 @@ public function testInlineLevel()
foobar: { foo: bar, bar: [1, foo], foobar: { foo: bar, bar: [1, foo] } }
EOF;
- $this->assertEquals($expected, $this->dumper->dump($this->array, 1), '->dump() takes an inline level argument');
+ $this->assertSame($expected, $this->dumper->dump($this->array, 1), '->dump() takes an inline level argument');
+ $this->assertSameData($this->array, $this->parser->parse($expected));
$expected = <<<'EOF'
'': bar
@@ -138,7 +145,8 @@ public function testInlineLevel()
foobar: { foo: bar, bar: [1, foo] }
EOF;
- $this->assertEquals($expected, $this->dumper->dump($this->array, 2), '->dump() takes an inline level argument');
+ $this->assertSame($expected, $this->dumper->dump($this->array, 2), '->dump() takes an inline level argument');
+ $this->assertSameData($this->array, $this->parser->parse($expected));
$expected = <<<'EOF'
'': bar
@@ -159,7 +167,8 @@ public function testInlineLevel()
bar: [1, foo]
EOF;
- $this->assertEquals($expected, $this->dumper->dump($this->array, 3), '->dump() takes an inline level argument');
+ $this->assertSame($expected, $this->dumper->dump($this->array, 3), '->dump() takes an inline level argument');
+ $this->assertSameData($this->array, $this->parser->parse($expected));
$expected = <<<'EOF'
'': bar
@@ -182,22 +191,23 @@ public function testInlineLevel()
- foo
EOF;
- $this->assertEquals($expected, $this->dumper->dump($this->array, 4), '->dump() takes an inline level argument');
- $this->assertEquals($expected, $this->dumper->dump($this->array, 10), '->dump() takes an inline level argument');
+ $this->assertSame($expected, $this->dumper->dump($this->array, 4), '->dump() takes an inline level argument');
+ $this->assertSame($expected, $this->dumper->dump($this->array, 10), '->dump() takes an inline level argument');
+ $this->assertSameData($this->array, $this->parser->parse($expected));
}
public function testObjectSupportEnabled()
{
$dump = $this->dumper->dump(['foo' => new A(), 'bar' => 1], 0, 0, Yaml::DUMP_OBJECT);
- $this->assertEquals('{ foo: !php/object \'O:30:"Symfony\Component\Yaml\Tests\A":1:{s:1:"a";s:3:"foo";}\', bar: 1 }', $dump, '->dump() is able to dump objects');
+ $this->assertSame('{ foo: !php/object \'O:30:"Symfony\Component\Yaml\Tests\A":1:{s:1:"a";s:3:"foo";}\', bar: 1 }', $dump, '->dump() is able to dump objects');
}
public function testObjectSupportDisabledButNoExceptions()
{
$dump = $this->dumper->dump(['foo' => new A(), 'bar' => 1]);
- $this->assertEquals('{ foo: null, bar: 1 }', $dump, '->dump() does not dump objects when disabled');
+ $this->assertSame('{ foo: null, bar: 1 }', $dump, '->dump() does not dump objects when disabled');
}
public function testObjectSupportDisabledWithExceptions()
@@ -211,7 +221,8 @@ public function testObjectSupportDisabledWithExceptions()
*/
public function testEscapedEscapeSequencesInQuotedScalar($input, $expected)
{
- $this->assertEquals($expected, $this->dumper->dump($input));
+ $this->assertSame($expected, $this->dumper->dump($input));
+ $this->assertSameData($input, $this->parser->parse($expected));
}
public function getEscapeSequences()
@@ -261,7 +272,7 @@ public function testDumpObjectAsMap($object, $expected)
{
$yaml = $this->dumper->dump($object, 0, 0, Yaml::DUMP_OBJECT_AS_MAP);
- $this->assertEquals($expected, Yaml::parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP));
+ $this->assertSameData($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP));
}
public function objectAsMapProvider()
@@ -339,7 +350,7 @@ public function testDumpingArrayObjectInstancesWithNumericKeysRespectsInlineLeve
2: { 0: d, 1: e }
YAML;
- $this->assertEquals($expected, $yaml);
+ $this->assertSame($expected, $yaml);
}
public function testDumpEmptyArrayObjectInstanceAsMap()
@@ -378,6 +389,7 @@ public function testDumpingStdClassInstancesRespectsInlineLevel()
YAML;
$this->assertSame($expected, $yaml);
+ $this->assertSameData($outer, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP));
}
public function testDumpingTaggedValueSequenceRespectsInlineLevel()
@@ -403,6 +415,40 @@ public function testDumpingTaggedValueSequenceRespectsInlineLevel()
YAML;
$this->assertSame($expected, $yaml);
+ $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS));
+ }
+
+ public function testDumpingTaggedValueTopLevelScalar()
+ {
+ $data = new TaggedValue('user', 'jane');
+
+ $yaml = $this->dumper->dump($data);
+
+ $expected = '!user jane';
+ $this->assertSame($expected, $yaml);
+ $this->assertSameData($data, $this->parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS));
+ }
+
+ public function testDumpingTaggedValueTopLevelAssocInline()
+ {
+ $data = new TaggedValue('user', ['name' => 'jane']);
+
+ $yaml = $this->dumper->dump($data);
+
+ $expected = '!user { name: jane }';
+ $this->assertSame($expected, $yaml);
+ $this->assertSameData($data, $this->parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS));
+ }
+
+ public function testDumpingTaggedValueSpecialCharsInTag()
+ {
+ // @todo Validate the tag name in the TaggedValue constructor.
+ $data = new TaggedValue('a b @ c', 5);
+ $expected = '!a b @ c 5';
+ $this->assertSame($expected, $this->dumper->dump($data));
+ // The data changes after a round trip, due to the illegal tag name.
+ $data = new TaggedValue('a', 'b @ c 5');
+ $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS));
}
public function testDumpingTaggedValueSequenceWithInlinedTagValues()
@@ -415,6 +461,7 @@ public function testDumpingTaggedValueSequenceWithInlinedTagValues()
'john',
'claire',
]),
+ new TaggedValue('number', 5),
];
$yaml = $this->dumper->dump($data, 1);
@@ -422,9 +469,13 @@ public function testDumpingTaggedValueSequenceWithInlinedTagValues()
$expected = <<assertSame($expected, $yaml);
+ // @todo Fix the parser, preserve numbers.
+ $data[2] = new TaggedValue('number', '5');
+ $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS));
}
public function testDumpingTaggedValueMapRespectsInlineLevel()
@@ -437,6 +488,7 @@ public function testDumpingTaggedValueMapRespectsInlineLevel()
'john',
'claire',
]),
+ 'count' => new TaggedValue('number', 5),
];
$yaml = $this->dumper->dump($data, 2);
@@ -447,9 +499,13 @@ public function testDumpingTaggedValueMapRespectsInlineLevel()
names1: !names
- john
- claire
+count: !number 5
YAML;
$this->assertSame($expected, $yaml);
+ // @todo Fix the parser, preserve numbers.
+ $data['count'] = new TaggedValue('number', '5');
+ $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS));
}
public function testDumpingTaggedValueMapWithInlinedTagValues()
@@ -472,6 +528,7 @@ public function testDumpingTaggedValueMapWithInlinedTagValues()
YAML;
$this->assertSame($expected, $yaml);
+ $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS));
}
public function testDumpingNotInlinedScalarTaggedValue()
@@ -487,6 +544,7 @@ public function testDumpingNotInlinedScalarTaggedValue()
YAML;
$this->assertSame($expected, $this->dumper->dump($data, 2));
+ $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS));
}
public function testDumpingNotInlinedNullTaggedValue()
@@ -500,6 +558,10 @@ public function testDumpingNotInlinedNullTaggedValue()
YAML;
$this->assertSame($expected, $this->dumper->dump($data, 2));
+
+ // @todo Fix the parser, don't stringify null.
+ $data['foo'] = new TaggedValue('bar', 'null');
+ $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_CONSTANT));
}
public function testDumpingMultiLineStringAsScalarBlockTaggedValue()
@@ -519,6 +581,53 @@ public function testDumpingMultiLineStringAsScalarBlockTaggedValue()
' baz';
$this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+ $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS));
+ }
+
+ public function testDumpingTaggedMultiLineInList()
+ {
+ $data = [
+ new TaggedValue('bar', "a\nb"),
+ ];
+ $expected = "- !bar |\n a\n b";
+ $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+
+ // @todo Fix the parser, eliminate these exceptions.
+ $this->expectException(ParseException::class);
+ $this->expectExceptionMessage('Unable to parse at line 3 (near "!bar |").');
+
+ $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS);
+ }
+
+ public function testDumpingTaggedMultiLineTrailingNewlinesInMap()
+ {
+ $data = [
+ 'foo' => new TaggedValue('bar', "a\nb\n\n\n"),
+ ];
+ $expected = "foo: !bar |\n a\n b\n \n \n ";
+ $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+
+ // @todo Fix the parser, the result should be identical to $data.
+ $this->assertSameData(
+ [
+ 'foo' => new TaggedValue('bar', "a\nb\n"),
+ ],
+ $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS));
+ }
+
+ public function testDumpingTaggedMultiLineTrailingNewlinesInList()
+ {
+ $data = [
+ new TaggedValue('bar', "a\nb\n\n\n"),
+ ];
+ $expected = "- !bar |\n a\n b\n \n \n ";
+ $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+
+ // @todo Fix the parser, eliminate these exceptions.
+ $this->expectException(ParseException::class);
+ $this->expectExceptionMessage('Unable to parse at line 6 (near "!bar |").');
+
+ $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS);
}
public function testDumpingInlinedMultiLineIfRnBreakLineInTaggedValue()
@@ -528,8 +637,14 @@ public function testDumpingInlinedMultiLineIfRnBreakLineInTaggedValue()
'foo' => new TaggedValue('bar', "foo\r\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz"),
],
];
+ $expected = <<<'YAML'
+data:
+ foo: !bar "foo\r\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz"
- $this->assertSame(file_get_contents(__DIR__.'/Fixtures/multiple_lines_as_literal_block_for_tagged_values.yml'), $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+YAML;
+ $yml = $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
+ $this->assertSame($expected, $yml);
+ $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS));
}
public function testDumpMultiLineStringAsScalarBlock()
@@ -544,8 +659,27 @@ public function testDumpMultiLineStringAsScalarBlock()
],
],
];
-
- $this->assertSame(file_get_contents(__DIR__.'/Fixtures/multiple_lines_as_literal_block.yml'), $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+ $yml = $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
+ $expected = str_replace("@\n", "\n", <<<'YAML'
+data:
+ single_line: 'foo bar baz'
+ multi_line: |-
+ foo
+ line with trailing spaces:
+ @
+ bar
+ integer like line:
+ 123456789
+ empty line:
+
+ baz
+ multi_line_with_carriage_return: "foo\nbar\r\nbaz"
+ nested_inlined_multi_line_string: { inlined_multi_line: "foo\nbar\r\nempty line:\n\nbaz" }
+
+YAML
+);
+ $this->assertSame($expected, $yml);
+ $this->assertSame($data, $this->parser->parse($yml));
}
public function testDumpMultiLineStringAsScalarBlockWhenFirstLineHasLeadingSpace()
@@ -558,27 +692,33 @@ public function testDumpMultiLineStringAsScalarBlockWhenFirstLineHasLeadingSpace
$expected = "data:\n multi_line: |4-\n the first line has leading spaces\n The second line does not.";
- $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+ $yml = $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
+ $this->assertSame($expected, $yml);
+ $this->assertSame($data, $this->parser->parse($yml));
}
public function testCarriageReturnFollowedByNewlineIsMaintainedWhenDumpingAsMultiLineLiteralBlock()
{
- $this->assertSame("- \"a\\r\\nb\\nc\"\n", $this->dumper->dump(["a\r\nb\nc"], 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+ $data = ["a\r\nb\nc"];
+ $expected = "- \"a\\r\\nb\\nc\"\n";
+ $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+ $this->assertSame($data, $this->parser->parse($expected));
}
public function testCarriageReturnNotFollowedByNewlineIsPreservedWhenDumpingAsMultiLineLiteralBlock()
{
+ $data = [
+ 'parent' => [
+ 'foo' => "bar\n\rbaz: qux",
+ ],
+ ];
$expected = <<<'YAML'
parent:
foo: "bar\n\rbaz: qux"
YAML;
-
- $this->assertSame($expected, $this->dumper->dump([
- 'parent' => [
- 'foo' => "bar\n\rbaz: qux",
- ],
- ], 4, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+ $this->assertSame($expected, $this->dumper->dump($data, 4, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+ $this->assertSame($data, $this->parser->parse($expected));
}
public function testNoExtraTrailingNewlineWhenDumpingAsMultiLineLiteralBlock()
@@ -590,7 +730,15 @@ public function testNoExtraTrailingNewlineWhenDumpingAsMultiLineLiteralBlock()
$yaml = $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$this->assertSame("- |-\n a\n b\n- |-\n c\n d", $yaml);
- $this->assertSame($data, Yaml::parse($yaml));
+ $this->assertSame($data, $this->parser->parse($yaml));
+ }
+
+ public function testTopLevelMultiLineStringLiteral()
+ {
+ $data = "a\nb\n";
+ $yaml = $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
+ $this->assertSame('"a\nb\n"', $yaml);
+ $this->assertSame($data, $this->parser->parse($yaml));
}
public function testDumpTrailingNewlineInMultiLineLiteralBlocks()
@@ -600,6 +748,7 @@ public function testDumpTrailingNewlineInMultiLineLiteralBlocks()
'clip 2' => "one\ntwo\n",
'keep 1' => "one\ntwo\n",
'keep 2' => "one\ntwo\n\n",
+ 'keep 3' => "one\ntwo\n\n\n",
'strip 1' => "one\ntwo",
'strip 2' => "one\ntwo",
];
@@ -619,6 +768,11 @@ public function testDumpTrailingNewlineInMultiLineLiteralBlocks()
one
two
+'keep 3': |+
+ one
+ two
+
+
'strip 1': |-
one
two
@@ -628,7 +782,7 @@ public function testDumpTrailingNewlineInMultiLineLiteralBlocks()
YAML;
$this->assertSame($expected, $yaml);
- $this->assertSame($data, Yaml::parse($yaml));
+ $this->assertSame($data, $this->parser->parse($yaml));
}
public function testZeroIndentationThrowsException()
@@ -664,6 +818,15 @@ public function testDumpIdeographicSpaces()
'regular_space' => 'a b',
], 2));
}
+
+ private function assertSameData($expected, $actual)
+ {
+ $this->assertEquals($expected, $actual);
+ $this->assertSame(
+ var_export($expected, true),
+ var_export($actual, true)
+ );
+ }
}
class A
diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block.yml
deleted file mode 100644
index 1f61eb1216a52..0000000000000
--- a/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-data:
- single_line: 'foo bar baz'
- multi_line: |-
- foo
- line with trailing spaces:
-
- bar
- integer like line:
- 123456789
- empty line:
-
- baz
- multi_line_with_carriage_return: "foo\nbar\r\nbaz"
- nested_inlined_multi_line_string: { inlined_multi_line: "foo\nbar\r\nempty line:\n\nbaz" }
diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block_for_tagged_values.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block_for_tagged_values.yml
deleted file mode 100644
index f8c9112fd52a5..0000000000000
--- a/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block_for_tagged_values.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-data:
- foo: !bar "foo\r\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz"
diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php
index 769af36eaa6dd..228c2f2ee9c69 100644
--- a/src/Symfony/Component/Yaml/Tests/ParserTest.php
+++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php
@@ -34,6 +34,90 @@ protected function tearDown(): void
chmod(__DIR__.'/Fixtures/not_readable.yml', 0644);
}
+ public function testTopLevelNumber()
+ {
+ $yml = '5';
+ $data = $this->parser->parse($yml);
+ $expected = 5;
+ $this->assertSameData($expected, $data);
+ }
+
+ public function testTopLevelNull()
+ {
+ $yml = 'null';
+ $data = $this->parser->parse($yml);
+ $expected = null;
+ $this->assertSameData($expected, $data);
+ }
+
+ public function testTaggedValueTopLevelNumber()
+ {
+ $yml = '!number 5';
+ $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS);
+ // @todo Preserve the number, don't turn into string.
+ $expected = new TaggedValue('number', '5');
+ $this->assertSameData($expected, $data);
+ }
+
+ public function testTaggedValueTopLevelNull()
+ {
+ $yml = '!tag null';
+ $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS);
+ // @todo Preserve literal null, don't turn into string.
+ $expected = new TaggedValue('tag', 'null');
+ $this->assertSameData($expected, $data);
+ }
+
+ public function testTaggedValueTopLevelString()
+ {
+ $yml = '!user barbara';
+ $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS);
+ $expected = new TaggedValue('user', 'barbara');
+ $this->assertSameData($expected, $data);
+ }
+
+ public function testTaggedValueTopLevelAssocInline()
+ {
+ $yml = '!user { name: barbara }';
+ $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS);
+ $expected = new TaggedValue('user', ['name' => 'barbara']);
+ $this->assertSameData($expected, $data);
+ }
+
+ public function testTaggedValueTopLevelAssoc()
+ {
+ $yml = <<<'YAML'
+!user
+name: barbara
+YAML;
+ $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS);
+ $expected = new TaggedValue('user', ['name' => 'barbara']);
+ $this->assertSameData($expected, $data);
+ }
+
+ public function testTaggedValueTopLevelList()
+ {
+ $yml = <<<'YAML'
+!users
+- barbara
+YAML;
+ $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS);
+ $expected = new TaggedValue('users', ['barbara']);
+ $this->assertSameData($expected, $data);
+ }
+
+ public function testTaggedTextAsListItem()
+ {
+ $yml = <<<'YAML'
+- !text |
+ first line
+YAML;
+ // @todo Fix the parser, eliminate this exception.
+ $this->expectException(ParseException::class);
+ $this->expectExceptionMessage('Unable to parse at line 2 (near "!text |").');
+ $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS);
+ }
+
/**
* @dataProvider getDataFormSpecifications
*/
@@ -104,7 +188,7 @@ public function testParserIsStateless()
public function testValidTokenSeparation(string $given, array $expected)
{
$actual = $this->parser->parse($given);
- $this->assertEquals($expected, $actual);
+ $this->assertSameData($expected, $actual);
}
public function validTokenSeparators(): array
@@ -482,7 +566,7 @@ public function testObjectSupportEnabled()
foo: !php/object O:30:"Symfony\Component\Yaml\Tests\B":1:{s:1:"b";s:3:"foo";}
bar: 1
EOF;
- $this->assertEquals(['foo' => new B(), 'bar' => 1], $this->parser->parse($input, Yaml::PARSE_OBJECT), '->parse() is able to parse objects');
+ $this->assertSameData(['foo' => new B(), 'bar' => 1], $this->parser->parse($input, Yaml::PARSE_OBJECT), '->parse() is able to parse objects');
}
public function testObjectSupportDisabledButNoExceptions()
@@ -491,7 +575,7 @@ public function testObjectSupportDisabledButNoExceptions()
foo: !php/object O:30:"Symfony\Tests\Component\Yaml\B":1:{s:1:"b";s:3:"foo";}
bar: 1
EOF;
- $this->assertEquals(['foo' => null, 'bar' => 1], $this->parser->parse($input), '->parse() does not parse objects');
+ $this->assertSameData(['foo' => null, 'bar' => 1], $this->parser->parse($input), '->parse() does not parse objects');
}
/**
@@ -501,7 +585,7 @@ public function testObjectForMap($yaml, $expected)
{
$flags = Yaml::PARSE_OBJECT_FOR_MAP;
- $this->assertEquals($expected, $this->parser->parse($yaml, $flags));
+ $this->assertSameData($expected, $this->parser->parse($yaml, $flags));
}
public function getObjectForMapTests()
@@ -956,12 +1040,12 @@ public function testEmptyValue()
hash:
EOF;
- $this->assertEquals(['hash' => null], Yaml::parse($input));
+ $this->assertSame(['hash' => null], Yaml::parse($input));
}
public function testCommentAtTheRootIndent()
{
- $this->assertEquals([
+ $this->assertSame([
'services' => [
'app.foo_service' => [
'class' => 'Foo',
@@ -987,7 +1071,7 @@ class: Bar
public function testStringBlockWithComments()
{
- $this->assertEquals(['content' => <<<'EOT'
+ $this->assertSame(['content' => <<<'EOT'
# comment 1
header
@@ -1015,7 +1099,7 @@ public function testStringBlockWithComments()
public function testFoldedStringBlockWithComments()
{
- $this->assertEquals([['content' => <<<'EOT'
+ $this->assertSame([['content' => <<<'EOT'
# comment 1
header
@@ -1044,7 +1128,7 @@ public function testFoldedStringBlockWithComments()
public function testNestedFoldedStringBlockWithComments()
{
- $this->assertEquals([[
+ $this->assertSame([[
'title' => 'some title',
'content' => <<<'EOT'
# comment 1
@@ -1076,7 +1160,7 @@ public function testNestedFoldedStringBlockWithComments()
public function testReferenceResolvingInInlineStrings()
{
- $this->assertEquals([
+ $this->assertSame([
'var' => 'var-value',
'scalar' => 'var-value',
'list' => ['var-value'],
@@ -1116,7 +1200,7 @@ public function testYamlDirective()
foo: 1
bar: 2
EOF;
- $this->assertEquals(['foo' => 1, 'bar' => 2], $this->parser->parse($yaml));
+ $this->assertSame(['foo' => 1, 'bar' => 2], $this->parser->parse($yaml));
}
public function testFloatKeys()
@@ -1166,7 +1250,7 @@ public function testExplicitStringCasting()
'~' => 'null',
];
- $this->assertEquals($expected, $this->parser->parse($yaml));
+ $this->assertSame($expected, $this->parser->parse($yaml));
}
public function testColonInMappingValueException()
@@ -1467,7 +1551,7 @@ public function testParseDateAsMappingValue()
$expectedDate->setDate(2002, 12, 14);
$expectedDate->setTime(0, 0, 0);
- $this->assertEquals(['date' => $expectedDate], $this->parser->parse($yaml, Yaml::PARSE_DATETIME));
+ $this->assertSameData(['date' => $expectedDate], $this->parser->parse($yaml, Yaml::PARSE_DATETIME));
}
/**
@@ -1687,7 +1771,7 @@ public function testBackslashInSingleQuotedString()
public function testParseMultiLineString()
{
- $this->assertEquals("foo bar\nbaz", $this->parser->parse("foo\nbar\n\nbaz"));
+ $this->assertSame("foo bar\nbaz", $this->parser->parse("foo\nbar\n\nbaz"));
}
/**
@@ -1695,7 +1779,7 @@ public function testParseMultiLineString()
*/
public function testParseMultiLineMappingValue($yaml, $expected, $parseError)
{
- $this->assertEquals($expected, $this->parser->parse($yaml));
+ $this->assertSame($expected, $this->parser->parse($yaml));
}
public function multiLineDataProvider()
@@ -1762,7 +1846,7 @@ public function multiLineDataProvider()
*/
public function testInlineNotationSpanningMultipleLines($expected, string $yaml)
{
- $this->assertEquals($expected, $this->parser->parse($yaml));
+ $this->assertSame($expected, $this->parser->parse($yaml));
}
public function inlineNotationSpanningMultipleLinesProvider(): array
@@ -2136,7 +2220,7 @@ public function testRootLevelInlineMappingFollowedByMoreContentIsInvalid()
public function testTaggedInlineMapping()
{
- $this->assertEquals(new TaggedValue('foo', ['foo' => 'bar']), $this->parser->parse('!foo {foo: bar}', Yaml::PARSE_CUSTOM_TAGS));
+ $this->assertSameData(new TaggedValue('foo', ['foo' => 'bar']), $this->parser->parse('!foo {foo: bar}', Yaml::PARSE_CUSTOM_TAGS));
}
public function testInvalidInlineSequenceContainingStringWithEscapedQuotationCharacter()
@@ -2151,7 +2235,7 @@ public function testInvalidInlineSequenceContainingStringWithEscapedQuotationCha
*/
public function testCustomTagSupport($expected, $yaml)
{
- $this->assertEquals($expected, $this->parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS));
+ $this->assertSameData($expected, $this->parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS));
}
public function taggedValuesProvider()
@@ -2347,7 +2431,7 @@ public function testCanParseVeryLongValue()
$yamlString = Yaml::dump($trickyVal);
$arrayFromYaml = $this->parser->parse($yamlString);
- $this->assertEquals($trickyVal, $arrayFromYaml);
+ $this->assertSame($trickyVal, $arrayFromYaml);
}
public function testParserCleansUpReferencesBetweenRuns()
@@ -2462,7 +2546,7 @@ public function testMergeKeysWhenMappingsAreParsedAsObjects()
],
];
- $this->assertEquals($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP));
+ $this->assertSameData($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP));
}
public function testFilenamesAreParsedAsStringsWithoutFlag()
@@ -2555,7 +2639,7 @@ public function testParseReferencesOnMergeKeysWithMappingsParsedAsObjects()
],
];
- $this->assertEquals($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP));
+ $this->assertSameData($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP));
}
public function testEvalRefException()
@@ -2830,6 +2914,15 @@ public function testParseIdeographicSpaces()
'regular_space' => 'a b',
], $this->parser->parse($expected));
}
+
+ private function assertSameData($expected, $actual)
+ {
+ $this->assertEquals($expected, $actual);
+ $this->assertSame(
+ var_export($expected, true),
+ var_export($actual, true)
+ );
+ }
}
class B