Merge branch '4.4'

* 4.4: (39 commits)
  [Console] Fix #33915, Detect dimensions using mode CON if vt100 is supported
  [PhpUnitBridge] Also search for composer.phar in git root folder
  [HttpKernel][DataCollectorInterface] Ease compatibility
  Add tests to ensure defaultLocale is properly passed to the URL generator
  [DependencyInjection] Fix broken references in tests
  [VarDumper] display the method we're in when dumping stack traces
  [HttpClient] Retry safe requests when then fail before the body arrives
  [Console] Rename some methods related to redraw frequency
  Avoid using of kernel after shutdown
  Simplify PHP CS Fixer configuration
  [PropertyInfo] Fixed type extraction for nullable collections of non-nullable elements
  [FrameworkBundle] [HttpKernel] fixed correct EOL and EOM month
  Fix CS
  [Serializer] Fix property name usage for denormalization
  Name test accordingly to the tested class
  Fix MockFileSessionStorageTest::sessionDir being used after it's unset
  [Security] Fix SwitchUserToken wrongly deauthenticated
  Supporting Bootstrap 4 custom switches
  Add new Form WeekType
  bumped Symfony version to 4.3.7
  ...
This commit is contained in:
Nicolas Grekas 2019-11-05 17:53:02 +01:00
commit 5e358e33c9
136 changed files with 2988 additions and 190 deletions

View File

@ -10,17 +10,9 @@ return PhpCsFixer\Config::create()
'@Symfony:risky' => true,
'@PHPUnit75Migration:risky' => true,
'php_unit_dedicate_assert' => ['target' => '5.6'],
'phpdoc_no_empty_return' => false, // triggers almost always false positive
'array_syntax' => ['syntax' => 'short'],
'fopen_flags' => false,
'ordered_imports' => true,
'phpdoc_trim_consecutive_blank_line_separation' => true,
'no_superfluous_phpdoc_tags' => ['allow_mixed' => true],
'protected_to_private' => false,
// Part of @Symfony:risky in PHP-CS-Fixer 2.13.0. To be removed from the config file once upgrading
'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'namespaced', 'strict' => true],
// Part of future @Symfony ruleset in PHP-CS-Fixer To be removed from the config file once upgrading
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
'combine_nested_dirname' => true,
])
->setRiskyAllowed(true)

View File

@ -7,6 +7,49 @@ in 4.3 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/v4.3.0...v4.3.1
* 4.3.6 (2019-11-01)
* bug #34198 [HttpClient] Fix perf issue when doing thousands of requests with curl (nicolas-grekas)
* bug #33998 [Config] Disable default alphabet sorting in glob function due of unstable sort (hurricane-voronin)
* bug #34144 [Serializer] Improve messages for unexpected resources values (fancyweb)
* bug #34186 [HttpClient] always return the empty string when the response cannot have a body (nicolas-grekas)
* bug #34167 [HttpFoundation] Allow to not pass a parameter to Request::isMethodSafe() (dunglas)
* bug #33828 [DoctrineBridge] Auto-validation must work if no regex are passed (dunglas)
* bug #34080 [SecurityBundle] correct types for default arguments for firewall configs (shieldo)
* bug #34152 [Workflow] Made the configuration more robust for the 'property' key (lyrixx)
* bug #34154 [HttpClient] fix handling of 3xx with no Location header - ignore Content-Length when no body is expected (nicolas-grekas)
* bug #34140 [Security/Core] make NativePasswordEncoder use sodium to validate passwords when possible (nicolas-grekas)
* bug #33999 [Form] Make sure to collect child forms created on *_SET_DATA events (yceruto)
* bug #34090 [WebProfilerBundle] Improve display in Email panel for dark theme (antograssiot)
* bug #34116 [HttpClient] ignore the body of responses to HEAD requests (nicolas-grekas)
* bug #32456 [Messenger] use database platform to convert correctly the DateTime (roukmoute)
* bug #34107 [Messenger] prevent infinite redelivery loops and blocked queues (Tobion)
* bug #32341 [Messenger] Show exceptions after multiple retries (TimoBakx)
* bug #34082 Revert "[Messenger] Fix exception message of failed message is dropped (Tobion)
* bug #34021 [TwigBridge] do not render errors for checkboxes twice (xabbuh)
* bug #34017 [Messenger] Fix ignored options in redis transport (chalasr)
* bug #34041 [HttpKernel] fix wrong removal of the just generated container dir (nicolas-grekas)
* bug #34024 [Routing] fix route loading with wildcard, but dir or file is empty (gseidel)
* bug #34023 [Dotenv] allow LF in single-quoted strings (nicolas-grekas)
* bug #33818 [Yaml] Throw exception for tagged invalid inline elements (gharlan)
* bug #33994 [Mailer] Fix Mandrill Transport API payload for named addresses (Michaël Perrin)
* bug #33985 [HttpClient] workaround curl_multi_select() issue (nicolas-grekas)
* bug #33948 [PropertyInfo] Respect property name case when guessing from public method name (antograssiot)
* bug #33962 [Cache] fixed TagAwareAdapter returning invalid cache (v-m-i)
* bug #33958 [DI] Add extra type check to php dumper (gquemener)
* bug #33965 [HttpFoundation] Add plus character `+` to legal mime subtype (ilzrv)
* bug #32943 [Dotenv] search variable values in ENV first then env file (soufianZantar)
* bug #33943 [VarDumper] fix resetting the "bold" state in CliDumper (nicolas-grekas)
* bug #33936 [HttpClient] Missing argument in method_exists (detinkin)
* bug #33937 [Cache] ignore unserialization failures in AbstractTagAwareAdapter::doDelete() (nicolas-grekas)
* bug #33935 [HttpClient] send `Accept: */*` by default, fix removing it when needed (nicolas-grekas)
* bug #33922 [Cache] remove implicit dependency on symfony/filesystem (nicolas-grekas)
* bug #33927 Allow to set SameSite config to 'none' (ihmels)
* bug #33930 [Cache] clean tags folder on invalidation (nicolas-grekas)
* bug #33919 [VarDumper] fix array key error for class SymfonyCaster (zcodes)
* bug #33885 [Form][DateTimeImmutableToDateTimeTransformer] Preserve microseconds and use \DateTime::createFromImmutable() when available (fancyweb)
* bug #33900 [HttpKernel] Fix to populate $dotenvVars in data collector when not using putenv() (mynameisbogdan)
* 4.3.5 (2019-10-07)
* bug #33742 [Crawler] document $default as string|null (nicolas-grekas)

View File

@ -9,16 +9,16 @@ Symfony is the result of the work of many people who made the code better
- Christian Flothmann (xabbuh)
- Bernhard Schussek (bschussek)
- Tobias Schultze (tobion)
- Christophe Coevoet (stof)
- Robin Chalas (chalas_r)
- Jordi Boggiano (seldaek)
- Christophe Coevoet (stof)
- Kévin Dunglas (dunglas)
- Jordi Boggiano (seldaek)
- Victor Berchet (victor)
- Maxime Steinhausser (ogizanagi)
- Ryan Weaver (weaverryan)
- Jakub Zalas (jakubzalas)
- Javier Eguiluz (javier.eguiluz)
- Roland Franssen (ro0)
- Jakub Zalas (jakubzalas)
- Johannes S (johannes)
- Grégoire Pineau (lyrixx)
- Kris Wallsmith (kriswallsmith)
@ -31,8 +31,8 @@ Symfony is the result of the work of many people who made the code better
- Wouter De Jong (wouterj)
- Joseph Bielawski (stloyd)
- Karma Dordrak (drak)
- Lukas Kahwe Smith (lsmith)
- Alexander M. Turek (derrabus)
- Lukas Kahwe Smith (lsmith)
- Martin Hasoň (hason)
- Hamza Amrouche (simperfit)
- Jeremy Mikola (jmikola)
@ -42,9 +42,9 @@ Symfony is the result of the work of many people who made the code better
- Igor Wiedler (igorw)
- Jérémy DERUSSÉ (jderusse)
- Eriksen Costa (eriksencosta)
- Thomas Calvet (fancyweb)
- Guilhem Niot (energetick)
- Sarah Khalil (saro0h)
- Thomas Calvet (fancyweb)
- Tobias Nyholm (tobias)
- Jonathan Wage (jwage)
- Lynn van der Berg (kjarli)
@ -69,14 +69,14 @@ Symfony is the result of the work of many people who made the code better
- Miha Vrhovnik
- Diego Saint Esteben (dii3g0)
- Gábor Egyed (1ed)
- Gabriel Ostrolucký (gadelat)
- Titouan Galopin (tgalopin)
- Konstantin Kudryashov (everzet)
- David Maicher (dmaicher)
- Bilal Amarni (bamarni)
- Mathieu Piot (mpiot)
- Gabriel Ostrolucký (gadelat)
- Florin Patan (florinpatan)
- Vladimir Reznichenko (kalessil)
- Florin Patan (florinpatan)
- Jáchym Toušek (enumag)
- Michel Weimerskirch (mweimerskirch)
- Andrej Hudec (pulzarraider)
@ -86,10 +86,10 @@ Symfony is the result of the work of many people who made the code better
- Jan Schädlich (jschaedl)
- Christian Raue
- Arnout Boks (aboks)
- Douglas Greenshields (shieldo)
- Deni
- Henrik Westphal (snc)
- Dariusz Górecki (canni)
- Douglas Greenshields (shieldo)
- David Buchmann (dbu)
- Dariusz Ruminski
- Lee McDermott
@ -126,12 +126,12 @@ Symfony is the result of the work of many people who made the code better
- Colin Frei
- Javier Spagnoletti (phansys)
- Joshua Thijssen
- Alex Pott
- Daniel Wehner (dawehner)
- excelwebzone
- Gordon Franke (gimler)
- Teoh Han Hui (teohhanhui)
- Oskar Stark (oskarstark)
- Alex Pott
- Fabien Pennequin (fabienpennequin)
- Théo FIDRY (theofidry)
- Eric GELOEN (gelo)
@ -140,6 +140,7 @@ Symfony is the result of the work of many people who made the code better
- Tugdual Saunier (tucksaun)
- Jannik Zschiesche (apfelbox)
- Robert Schönthal (digitalkaoz)
- Gregor Harlan (gharlan)
- Florian Lonqueu-Brochard (florianlb)
- Gabriel Caruso (carusogabriel)
- Stefano Sala (stefano.sala)
@ -163,8 +164,10 @@ Symfony is the result of the work of many people who made the code better
- Philipp Wahala (hifi)
- Rafael Dohms (rdohms)
- jwdeitch
- Alexander Schranz (alexander-schranz)
- Mikael Pajunen
- Alessandro Chitolina (alekitto)
- Yanick Witschi (toflar)
- Massimiliano Arione (garak)
- Niels Keurentjes (curry684)
- Vyacheslav Pavlov
@ -172,13 +175,10 @@ Symfony is the result of the work of many people who made the code better
- Richard Shank (iampersistent)
- Thomas Rabaix (rande)
- Vincent Touzet (vincenttouzet)
- Gregor Harlan (gharlan)
- jeremyFreeAgent (jeremyfreeagent)
- Rouven Weßling (realityking)
- Alexander Schranz (alexander-schranz)
- Clemens Tolboom
- Helmer Aaviksoo
- Yanick Witschi (toflar)
- Hiromi Hishida (77web)
- Matthieu Ouellette-Vachon (maoueh)
- Michał Pipa (michal.pipa)
@ -202,6 +202,7 @@ Symfony is the result of the work of many people who made the code better
- Daniel Espendiller
- Possum
- Dorian Villet (gnutix)
- Michaël Perrin (michael.perrin)
- Sergey Linnik (linniksa)
- Richard Miller (mr_r_miller)
- Albert Casademont (acasademont)
@ -209,6 +210,7 @@ Symfony is the result of the work of many people who made the code better
- Dennis Benkert (denderello)
- DQNEO
- mcfedr (mcfedr)
- Ben Davies (bendavies)
- Gary PEGEOT (gary-p)
- Ruben Gonzalez (rubenrua)
- Benjamin Dulau (dbenjamin)
@ -223,7 +225,6 @@ Symfony is the result of the work of many people who made the code better
- bronze1man
- sun (sun)
- Larry Garfield (crell)
- Michaël Perrin (michael.perrin)
- Nikolay Labinskiy (e-moe)
- Martin Schuhfuß (usefulthink)
- apetitpa
@ -232,11 +233,11 @@ Symfony is the result of the work of many people who made the code better
- Pierre Minnieur (pminnieur)
- fivestar
- Dominique Bongiraud
- Andre Rømcke (andrerom)
- Jeremy Livingston (jeremylivingston)
- Michael Lee (zerustech)
- Matthieu Auger (matthieuauger)
- Leszek Prabucki (l3l0)
- Ben Davies (bendavies)
- Fabien Bourigault (fbourigault)
- François Zaninotto (fzaninotto)
- Dustin Whittle (dustinwhittle)
@ -260,7 +261,6 @@ Symfony is the result of the work of many people who made the code better
- Mantis Development
- Loïc Faugeron
- Hidde Wieringa (hiddewie)
- Andre Rømcke (andrerom)
- Marco Pivetta (ocramius)
- Rob Frawley 2nd (robfrawley)
- julien pauli (jpauli)
@ -399,8 +399,10 @@ Symfony is the result of the work of many people who made the code better
- Vitaliy Zakharov (zakharovvi)
- Tobias Sjösten (tobiassjosten)
- Gyula Sallai (salla)
- Maciej Malarz (malarzm)
- Inal DJAFAR (inalgnu)
- Christian Gärtner (dagardner)
- Dmytro Borysovskyi (dmytr0)
- Tomasz Kowalczyk (thunderer)
- Artur Eshenbrener
- Damien Alexandre (damienalexandre)
@ -425,6 +427,7 @@ Symfony is the result of the work of many people who made the code better
- Grzegorz (Greg) Zdanowski (kiler129)
- Iker Ibarguren (ikerib)
- Kirill chEbba Chebunin (chebba)
- Anthony GRASSIOT (antograssiot)
- Greg Thornton (xdissent)
- Martin Hujer (martinhujer)
- Alex Bowers
@ -492,6 +495,7 @@ Symfony is the result of the work of many people who made the code better
- lancergr
- Mihai Stancu
- Ivan Nikolaev (destillat)
- Gildas Quéméner (gquemener)
- Olivier Dolbeau (odolbeau)
- Jan Rosier (rosier)
- Alessandro Lai (jean85)
@ -507,10 +511,8 @@ Symfony is the result of the work of many people who made the code better
- Sylvain Fabre (sylfabre)
- Martijn Cuppens
- Vlad Gregurco (vgregurco)
- Maciej Malarz (malarzm)
- Boris Vujicic (boris.vujicic)
- Chris Sedlmayr (catchamonkey)
- Dmytro Borysovskyi (dmytr0)
- Kamil Kokot (pamil)
- Seb Koelen
- Christoph Mewes (xrstf)
@ -522,6 +524,7 @@ Symfony is the result of the work of many people who made the code better
- Jonas Flodén (flojon)
- Tobias Weichart
- Gonzalo Vilaseca (gonzalovilaseca)
- Tarmo Leppänen (tarlepp)
- Marcin Sikoń (marphi)
- Tien Vo (tienvx)
- Denis Brumann (dbrumann)
@ -534,6 +537,7 @@ Symfony is the result of the work of many people who made the code better
- Gintautas Miselis
- Rob Bast
- Roberto Espinoza (respinoza)
- Soufian EZ-ZANTAR (soezz)
- Zander Baldwin
- Gocha Ossinkine (ossinkine)
- Adam Harvey
@ -592,6 +596,7 @@ Symfony is the result of the work of many people who made the code better
- Andrew Udvare (audvare)
- alexpods
- Saif Eddin G
- Johann Pardanaud
- Adam Szaraniec (mimol)
- Dariusz Ruminski
- Erik Trapman (eriktrapman)
@ -672,10 +677,10 @@ Symfony is the result of the work of many people who made the code better
- Sebastian Blum
- Alexis Lefebvre
- aubx
- Julien Turby
- Marvin Butkereit
- Renan
- Ricky Su (ricky)
- Gildas Quéméner (gquemener)
- Kyle Evans (kevans91)
- Charles-Henri Bruyand
- Max Rath (drak3)
@ -717,8 +722,10 @@ Symfony is the result of the work of many people who made the code better
- zenmate
- Michal Trojanowski
- David Fuhr
- Mathias STRASSER (roukmoute)
- Max Grigorian (maxakawizard)
- DerManoMann
- Timo Bakx (timobakx)
- Rostyslav Kinash
- Dennis Fridrich (dfridrich)
- Mardari Dorel (dorumd)
@ -798,6 +805,7 @@ Symfony is the result of the work of many people who made the code better
- Raphaëll Roussel
- Michael Lutz
- jochenvdv
- Reedy
- Arturas Smorgun (asarturas)
- Alexander Volochnev (exelenz)
- Michael Piecko
@ -816,12 +824,10 @@ Symfony is the result of the work of many people who made the code better
- Sebastian Grodzicki (sgrodzicki)
- Jeroen van den Enden (stoefke)
- Pascal Helfenstein
- Anthony GRASSIOT (antograssiot)
- Baldur Rensch (brensch)
- Pierre Rineau
- Vladyslav Petrovych
- Alex Xandra Albert Sim
- Soufian EZ-ZANTAR (soezz)
- Carson Full
- Sergey Yastrebov
- Trent Steel (trsteel88)
@ -861,7 +867,6 @@ Symfony is the result of the work of many people who made the code better
- Andrew Hilobok (hilobok)
- Noah Heck (myesain)
- Christian Soronellas (theunic)
- Johann Pardanaud
- fedor.f
- Yosmany Garcia (yosmanyga)
- Wouter de Wild
@ -943,6 +948,7 @@ Symfony is the result of the work of many people who made the code better
- Fabien LUCAS (flucas2)
- Omar Yepez (oyepez003)
- mwsaz
- bogdan
- Jelle Kapitein
- Benoît Bourgeois
- mantulo
@ -1009,7 +1015,6 @@ Symfony is the result of the work of many people who made the code better
- LOUARDI Abdeltif (ouardisoft)
- Robert Gruendler (pulse00)
- Simon Terrien (sterrien)
- Tarmo Leppänen (tarlepp)
- Benoît Merlet (trompette)
- Koen Kuipers
- datibbaw
@ -1047,7 +1052,6 @@ Symfony is the result of the work of many people who made the code better
- neghmurken
- xaav
- Mahmoud Mostafa (mahmoud)
- Julien Turby
- Ahmed Abdou
- Daniel Iwaniec
- Pieter
@ -1137,7 +1141,6 @@ Symfony is the result of the work of many people who made the code better
- Mert Simsek (mrtsmsk0)
- Lin Clark
- Jeremy David (jeremy.david)
- Timo Bakx (timobakx)
- Jordi Rejas
- Troy McCabe
- Ville Mattila
@ -1153,7 +1156,9 @@ Symfony is the result of the work of many people who made the code better
- nacho
- Piotr Antosik (antek88)
- Artem Lopata
- Vedran Mihočinec (v-m-i)
- Sergey Novikov (s12v)
- creiner
- Marcos Quesada (marcos_quesada)
- Matthew Vickery (mattvick)
- MARYNICH Mikhail (mmarynich-ext)
@ -1198,6 +1203,7 @@ Symfony is the result of the work of many people who made the code better
- Alex Demchenko (pilot)
- Tadas Gliaubicas (tadcka)
- Thanos Polymeneas (thanos)
- Atthaphon Urairat
- Benoit Garret
- Maximilian Ruta (deltachaos)
- Jakub Sacha
@ -1221,13 +1227,13 @@ Symfony is the result of the work of many people who made the code better
- James Hudson
- Stephen Clouse
- e-ivanov
- Michał (bambucha15)
- Einenlum
- Jochen Bayer (jocl)
- Patrick Carlo-Hickman
- Bruno MATEU
- Jeremy Bush
- wizhippo
- Mathias STRASSER (roukmoute)
- Thomason, James
- Gordienko Vladislav
- marie
@ -1328,6 +1334,7 @@ Symfony is the result of the work of many people who made the code better
- Jelte Steijaert (jelte)
- David Négrier (moufmouf)
- Quique Porta (quiqueporta)
- mohammadreza honarkhah
- stoccc
- Andrea Quintino (dirk39)
- Tomasz Szymczyk (karion)
@ -1338,6 +1345,7 @@ Symfony is the result of the work of many people who made the code better
- ConneXNL
- Aharon Perkel
- matze
- Justin Reherman (jreherman)
- Rubén Calvo (rubencm)
- Abdul.Mohsen B. A. A
- Swen van Zanten
@ -1364,6 +1372,7 @@ Symfony is the result of the work of many people who made the code better
- Erika Heidi Reinaldo (erikaheidi)
- Pierre Tachoire (krichprollsch)
- Marc J. Schmidt (marcjs)
- František Maša
- Sebastian Schwarz
- Marco Jantke
- Saem Ghani
@ -1378,6 +1387,7 @@ Symfony is the result of the work of many people who made the code better
- Walter Dal Mut (wdalmut)
- abluchet
- Ruud Arentsen
- Harald Tollefsen
- Matthieu
- Albin Kerouaton
- Sébastien HOUZÉ
@ -1388,6 +1398,7 @@ Symfony is the result of the work of many people who made the code better
- Cédric Lahouste (rapotor)
- Samuel Vogel (samuelvogel)
- Alexey Kopytko (sanmai)
- Osayawe Ogbemudia Terry (terdia)
- Berat Doğan
- Guillaume LECERF
- Juanmi Rodriguez Cerón
@ -1447,6 +1458,7 @@ Symfony is the result of the work of many people who made the code better
- WedgeSama
- Felds Liscia
- Chihiro Adachi (chihiro-adachi)
- Alex Bacart
- Raphaëll Roussel
- Tadcka
- Beth Binkovitz
@ -1627,6 +1639,7 @@ Symfony is the result of the work of many people who made the code better
- Matthew Foster (mfoster)
- Reyo Stallenberg (reyostallenberg)
- Paul Seiffert (seiffert)
- Simon Podlipsky (simpod)
- Vasily Khayrulin (sirian)
- Stefan Koopmanschap (skoop)
- Stas Soroka (stasyan)
@ -1647,6 +1660,7 @@ Symfony is the result of the work of many people who made the code better
- Phil Davis
- Gleb Sidora
- David Stone
- Gerhard Seidel (gseidel)
- Jovan Perovic (jperovic)
- Pablo Maria Martelletti (pmartelletti)
- Yassine Guedidi (yguedidi)
@ -1681,6 +1695,7 @@ Symfony is the result of the work of many people who made the code better
- Vladimir Khramtsov (chrome)
- Gerd Christian Kunze (derdu)
- Christoph Nissle (derstoffel)
- Denys Voronin (hurricane)
- Ionel Scutelnicu (ionelscutelnicu)
- Mathieu Dewet (mdewet)
- Nicolas Tallefourtané (nicolab)
@ -1770,6 +1785,7 @@ Symfony is the result of the work of many people who made the code better
- Daan van Renterghem
- Nicole Cordes
- Martin Kirilov
- Bálint Szekeres
- amcastror
- Alexander Li (aweelex)
- Bram Van der Sype (brammm)
@ -1785,6 +1801,7 @@ Symfony is the result of the work of many people who made the code better
- Dmitry Korotovsky
- mcorteel
- Michael van Tricht
- Ivan
- ReScO
- Tim Strehle
- Sam Ward
@ -1857,6 +1874,7 @@ Symfony is the result of the work of many people who made the code better
- Adrian
- Oleg Andreyev
- neFAST
- zcodes
- Pierre Rineau
- Florian Morello
- Maxim Lovchikov
@ -1911,6 +1929,7 @@ Symfony is the result of the work of many people who made the code better
- insidestyles
- Maerlyn
- Even André Fiskvik
- Agata
- Александр Ли
- Arjan Keeman
- Erik van Wingerden
@ -1959,6 +1978,7 @@ Symfony is the result of the work of many people who made the code better
- Christophe BECKER (goabonga)
- gondo (gondo)
- Gusakov Nikita (hell0w0rd)
- Yannick Ihmels (ihmels)
- Osman Üngür (import)
- Javier Núñez Berrocoso (javiernuber)
- Jelle Bekker (jbekker)
@ -1978,6 +1998,7 @@ Symfony is the result of the work of many people who made the code better
- Cayetano Soriano Gallego (neoshadybeat)
- Olivier Laviale (olvlvl)
- Ondrej Machulda (ondram)
- Pierre Gasté (pierre_g)
- Pablo Monterde Perez (plebs)
- Jimmy Leger (redpanda)
- Marcin Szepczynski (szepczynski)
@ -2046,6 +2067,7 @@ Symfony is the result of the work of many people who made the code better
- Myke79
- Brian Debuire
- Benjamin Morel
- Eric Grimois
- Piers Warmers
- Guilliam Xavier
- Sylvain Lorinet
@ -2075,6 +2097,7 @@ Symfony is the result of the work of many people who made the code better
- Jörg Rühl
- wesleyh
- sergey
- Menno Holtkamp
- Michael Hudson-Doyle
- Daniel Bannert
- Karim Miladi
@ -2111,6 +2134,7 @@ Symfony is the result of the work of many people who made the code better
- Daniel STANCU
- Ryan Rud
- Ondrej Slinták
- Rimas Kudelis
- vlechemin
- Brian Corrigan
- Ladislav Tánczos
@ -2165,6 +2189,7 @@ Symfony is the result of the work of many people who made the code better
- Ilya Bulakh
- David Soria Parra
- Sergiy Sokolenko
- detinkin
- Ahmed Abdulrahman
- dinitrol
- Penny Leach
@ -2193,6 +2218,7 @@ Symfony is the result of the work of many people who made the code better
- phc
- Дмитрий Пацура
- ilyes kooli
- Ilia Lazarev
- Michaël VEROUX
- Julia
- Lin Lu
@ -2312,6 +2338,7 @@ Symfony is the result of the work of many people who made the code better
- Alexander Menshchikov (zmey_kk)
- Florent Cailhol
- szymek
- Ryan Linnit
- Kovacs Nicolas
- craigmarvelley
- Stano Turza

View File

@ -19,6 +19,11 @@ Debug
* Deprecated the `FlattenException` class, use the one from the `ErrorRenderer` component instead
* Deprecated the component in favor of the `ErrorHandler` component
Config
------
* Deprecated overriding the `FilerLoader::import()` method without declaring the optional `$exclude` argument
DependencyInjection
-------------------

View File

@ -27,6 +27,7 @@ Config
* Removed `FileLoaderLoadException`, use `LoaderLoadException` instead.
* Using environment variables with `cannotBeEmpty()` if the value is validated with `validate()` will throw an exception.
* Removed the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead
* The `FilerLoader::import()` method has a new `$exclude` argument.
Console
-------
@ -390,6 +391,7 @@ Routing
with the new serialization methods in PHP 7.4.
* Removed `ServiceRouterLoader` and `ObjectRouteLoader`.
* Service route loaders must be tagged with `routing.route_loader`.
* The `RoutingConfigurator::import()` method has a new optional `$exclude` argument.
Security
--------

View File

@ -99,6 +99,7 @@ foreach ($defaultEnvs as $envName => $envValue) {
$COMPOSER = file_exists($COMPOSER = $oldPwd.'/composer.phar')
|| ($COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar`) : `which composer.phar 2> /dev/null`))
|| ($COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer`) : `which composer 2> /dev/null`))
|| file_exists($COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? `git rev-parse --show-toplevel 2> NUL` : `git rev-parse --show-toplevel 2> /dev/null`).DIRECTORY_SEPARATOR.'composer.phar')
? $PHP.' '.escapeshellarg($COMPOSER)
: 'composer';

View File

@ -18,6 +18,7 @@ CHANGELOG
* the `LintCommand` lints all the templates stored in all configured Twig paths if none argument is provided
* deprecated accepting STDIN implicitly when using the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit.
* added `--show-deprecations` option to the `lint:twig` command
* added support for Bootstrap4 switches, use `switch-custom` as `label_attr` in a `CheckboxType`
4.3.0
-----

View File

@ -166,6 +166,11 @@
<div class="custom-control custom-checkbox{{ 'checkbox-inline' in parent_label_class ? ' custom-control-inline' }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- elseif 'switch-custom' in parent_label_class -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' custom-control-input')|trim}) -%}
<div class="custom-control custom-switch{{ 'switch-inline' in parent_label_class ? ' custom-control-inline' }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- else -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|trim}) -%}
<div class="form-check{{ 'checkbox-inline' in parent_label_class ? ' form-check-inline' }}">
@ -238,7 +243,7 @@
{#- Do not display the label if widget is not defined in order to prevent double label rendering -#}
{%- if widget is defined -%}
{% set is_parent_custom = parent_label_class is defined and ('checkbox-custom' in parent_label_class or 'radio-custom' in parent_label_class) %}
{% set is_custom = label_attr.class is defined and ('checkbox-custom' in label_attr.class or 'radio-custom' in label_attr.class) %}
{% set is_custom = label_attr.class is defined and ('checkbox-custom' in label_attr.class or 'radio-custom' in label_attr.class or 'switch-custom' in label_attr.class) %}
{%- if is_parent_custom or is_custom -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' custom-control-label')|trim}) -%}
{%- else %}

View File

@ -172,7 +172,7 @@
{% block choice_label -%}
{# remove the checkbox-inline and radio-inline class, it's only useful for embed labels #}
{%- set label_attr = label_attr|merge({class: label_attr.class|default('')|replace({'checkbox-inline': '', 'radio-inline': '', 'checkbox-custom': '', 'radio-custom': ''})|trim}) -%}
{%- set label_attr = label_attr|merge({class: label_attr.class|default('')|replace({'checkbox-inline': '', 'radio-inline': '', 'checkbox-custom': '', 'radio-custom': '', 'switch-custom': ''})|trim}) -%}
{{- block('form_label') -}}
{% endblock choice_label %}

View File

@ -255,6 +255,17 @@
{{ block('form_widget_simple') }}
{%- endblock color_widget -%}
{%- block week_widget -%}
{%- if widget == 'single_text' -%}
{{ block('form_widget_simple') }}
{%- else -%}
{%- set vars = widget == 'text' ? { 'attr': { 'size': 1 }} : {} -%}
<div {{ block('widget_container_attributes') }}>
{{ form_widget(form.year, vars) }}-{{ form_widget(form.week, vars) }}
</div>
{%- endif -%}
{%- endblock week_widget -%}
{# Labels #}
{%- block form_label -%}

View File

@ -2703,6 +2703,104 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest
[@name="name"]
[@class="my&class form-control"]
[@value="#0000ff"]
'
);
}
public function testWeekSingleText()
{
$this->requiresFeatureSet(404);
$form = $this->factory->createNamed('holidays', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '1970-W01', [
'input' => 'string',
'widget' => 'single_text',
]);
$this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']],
'/input
[@type="week"]
[@name="holidays"]
[@class="my&class form-control"]
[@value="1970-W01"]
[not(@maxlength)]
'
);
}
public function testWeekSingleTextNoHtml5()
{
$this->requiresFeatureSet(404);
$form = $this->factory->createNamed('holidays', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '1970-W01', [
'input' => 'string',
'widget' => 'single_text',
'html5' => false,
]);
$this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']],
'/input
[@type="text"]
[@name="holidays"]
[@class="my&class form-control"]
[@value="1970-W01"]
[not(@maxlength)]
'
);
}
public function testWeekChoices()
{
$this->requiresFeatureSet(404);
$data = ['year' => date('Y'), 'week' => 1];
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\WeekType', $data, [
'input' => 'array',
'required' => false,
]);
$this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']],
'/div
[@class="my&class"]
[
./select
[@id="name_year"]
[@class="form-control"]
[./option[@value="'.$data['year'].'"][@selected="selected"]]
/following-sibling::select
[@id="name_week"]
[@class="form-control"]
[./option[@value="'.$data['week'].'"][@selected="selected"]]
]
[count(.//select)=2]'
);
}
public function testWeekText()
{
$this->requiresFeatureSet(404);
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '2000-W01', [
'input' => 'string',
'widget' => 'text',
]);
$this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']],
'/div
[@class="my&class"]
[
./input
[@id="name_year"]
[@type="number"]
[@class="form-control"]
[@value="2000"]
/following-sibling::input
[@id="name_week"]
[@type="number"]
[@class="form-control"]
[@value="1"]
]
[count(./input)=2]
'
);
}

View File

@ -24,6 +24,7 @@ CHANGELOG
4.4.0
-----
* Added `lint:container` command to check that services wiring matches type declarations
* Added `MailerAssertionsTrait`
* Deprecated support for `templating` engine in `TemplateController`, use Twig instead
* Deprecated the `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()`

View File

@ -125,7 +125,7 @@ EOT
private static function isExpired(string $date): bool
{
$date = \DateTime::createFromFormat('m/Y', $date);
$date = \DateTime::createFromFormat('d/m/Y', '01/'.$date);
return false !== $date && new \DateTime() > $date->modify('last day of this month 23:59:59');
}

View File

@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\Command;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
final class ContainerLintCommand extends Command
{
protected static $defaultName = 'lint:container';
/**
* @var ContainerBuilder
*/
private $containerBuilder;
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setDescription('Ensures that arguments injected into services match type declarations')
->setHelp('This command parses service definitions and ensures that injected values match the type declarations of each services\' class.')
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$container = $this->getContainerBuilder();
$container->setParameter('container.build_hash', 'lint_container');
$container->setParameter('container.build_time', time());
$container->setParameter('container.build_id', 'lint_container');
$container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100);
$container->compile();
return 0;
}
private function getContainerBuilder(): ContainerBuilder
{
if ($this->containerBuilder) {
return $this->containerBuilder;
}
$kernel = $this->getApplication()->getKernel();
if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) {
$buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel));
$container = $buildContainer();
$container->getCompilerPassConfig()->setRemovingPasses([]);
} else {
(new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump'));
}
return $this->containerBuilder = $container;
}
}

View File

@ -70,6 +70,10 @@
<tag name="console.command" command="debug:container" />
</service>
<service id="console.command.container_lint" class="Symfony\Bundle\FrameworkBundle\Command\ContainerLintCommand">
<tag name="console.command" command="lint:container" />
</service>
<service id="console.command.debug_autowiring" class="Symfony\Bundle\FrameworkBundle\Command\DebugAutowiringCommand">
<argument>null</argument>
<argument type="service" id="debug.file_link_formatter" on-invalid="null"/>

View File

@ -0,0 +1,14 @@
<?php if ($widget == 'single_text'): ?>
<?php echo $view['form']->block($form, 'form_widget_simple'); ?>
<?php else: ?>
<?php $vars = $widget == 'text' ? ['attr' => ['size' => 1]] : [] ?>
<div <?php echo $view['form']->block($form, 'widget_container_attributes') ?>>
<?php
// There should be no spaces between the colons and the widgets, that's why
// this block is written in a single PHP tag
echo $view['form']->widget($form['year'], $vars);
echo '-';
echo $view['form']->widget($form['week'], $vars);
?>
</div>
<?php endif ?>

View File

@ -42,6 +42,7 @@ abstract class KernelTestCase extends TestCase
protected function tearDown(): void
{
static::ensureKernelShutdown();
static::$kernel = null;
}
/**

View File

@ -13,6 +13,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\AnnotationReaderPass;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\Config\CustomConfig;
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@ -23,11 +24,16 @@ class TestBundle extends Bundle
{
parent::build($container);
$container->setParameter('container.build_hash', 'test_bundle');
$container->setParameter('container.build_time', time());
$container->setParameter('container.build_id', 'test_bundle');
/** @var $extension DependencyInjection\TestExtension */
$extension = $container->getExtension('test');
$extension->setCustomConfig(new CustomConfig());
$container->addCompilerPass(new AnnotationReaderPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle;
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class TestBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->setParameter('container.build_hash', 'test_bundle');
$container->setParameter('container.build_time', time());
$container->setParameter('container.build_id', 'test_bundle');
$container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100);
}
}

View File

@ -70,6 +70,6 @@ class JsonLoginTest extends AbstractWebTestCase
$this->assertSame(400, $response->getStatusCode());
$this->assertSame('application/json', $response->headers->get('Content-Type'));
$this->assertSame(['title' => 'Bad Request', 'status' => 400], json_decode($response->getContent(), true));
$this->assertSame(['title' => 'Bad Request', 'status' => 400, 'detail' => 'Whoops, looks like something went wrong.'], json_decode($response->getContent(), true));
}
}

View File

@ -12,9 +12,11 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\SecuredPageBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new SecuredPageBundle(),
new TestBundle(),
];

View File

@ -12,9 +12,11 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\EventBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new EventBundle(),
new TestBundle(),
];

View File

@ -13,4 +13,5 @@ return [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AutowiringBundle\AutowiringBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -14,4 +14,5 @@ return [
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\CsrfFormLoginBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -13,4 +13,5 @@ return [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FirewallEntryPointBundle\FirewallEntryPointBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -13,4 +13,5 @@ return [
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\JsonLoginBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -12,4 +12,5 @@
return [
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -11,8 +11,10 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new TestBundle(),
];

View File

@ -11,8 +11,10 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new TestBundle(),
];

View File

@ -12,9 +12,11 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\MissingUserProviderBundle\MissingUserProviderBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new MissingUserProviderBundle(),
new TestBundle(),
];

View File

@ -12,4 +12,5 @@
return [
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -11,8 +11,10 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new TestBundle(),
];

View File

@ -11,8 +11,10 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new TestBundle(),
];

View File

@ -12,6 +12,7 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\FormLoginBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
return [
@ -19,4 +20,5 @@ return [
new SecurityBundle(),
new TwigBundle(),
new FormLoginBundle(),
new TestBundle(),
];

View File

@ -9,6 +9,11 @@ CHANGELOG
* Added method `getChildNodeDefinitions()` to ParentNodeDefinitionInterface
* Removed `FileLoaderLoadException`, use `LoaderLoadException` instead
4.4.0
-----
* added a way to exclude patterns of resources from being imported by the `import()` method
4.3.0
-----

View File

@ -57,10 +57,11 @@ abstract class FileLoader extends Loader
/**
* Imports a resource.
*
* @param mixed $resource A Resource
* @param string|null $type The resource type or null if unknown
* @param bool $ignoreErrors Whether to ignore import errors or not
* @param string|null $sourceResource The original resource importing the new resource
* @param mixed $resource A Resource
* @param string|null $type The resource type or null if unknown
* @param bool $ignoreErrors Whether to ignore import errors or not
* @param string|null $sourceResource The original resource importing the new resource
* @param string|string[]|null $exclude Glob patterns to exclude from the import
*
* @return mixed
*
@ -68,12 +69,20 @@ abstract class FileLoader extends Loader
* @throws FileLoaderImportCircularReferenceException
* @throws FileLocatorFileNotFoundException
*/
public function import($resource, string $type = null, bool $ignoreErrors = false, string $sourceResource = null)
public function import($resource, string $type = null, bool $ignoreErrors = false, string $sourceResource = null, $exclude = null)
{
if (\is_string($resource) && \strlen($resource) !== $i = strcspn($resource, '*?{[')) {
$excluded = [];
foreach ((array) $exclude as $pattern) {
foreach ($this->glob($pattern, true, $_, false, true) as $path => $info) {
// normalize Windows slashes
$excluded[str_replace('\\', '/', $path)] = true;
}
}
$ret = [];
$isSubpath = 0 !== $i && false !== strpos(substr($resource, 0, $i), '/');
foreach ($this->glob($resource, false, $_, $ignoreErrors || !$isSubpath) as $path => $info) {
foreach ($this->glob($resource, false, $_, $ignoreErrors || !$isSubpath, false, $excluded) as $path => $info) {
if (null !== $res = $this->doImport($path, $type, $ignoreErrors, $sourceResource)) {
$ret[] = $res;
}

View File

@ -92,6 +92,14 @@ class FileLoaderTest extends TestCase
$this->assertSame(__FILE__, strtr($loader->import('FileLoaderTest.*'), '/', \DIRECTORY_SEPARATOR));
}
public function testImportWithExclude()
{
$loader = new TestFileLoader(new FileLocator(__DIR__.'/../Fixtures'));
$loadedFiles = $loader->import('Include/*', null, false, null, __DIR__.'/../Fixtures/Include/{ExcludeFile.txt}');
$this->assertCount(2, $loadedFiles);
$this->assertNotContains('ExcludeFile.txt', $loadedFiles);
}
}
class TestFileLoader extends FileLoader

View File

@ -20,7 +20,7 @@ CHANGELOG
* deprecated finding hidden commands using an abbreviation, use the full name instead
* added `Question::setTrimmable` default to true to allow the answer to be trimmed
* added method `preventRedrawFasterThan()` and `forceRedrawSlowerThan()` on `ProgressBar`
* added method `minSecondsBetweenRedraws()` and `maxSecondsBetweenRedraws()` on `ProgressBar`
* `Application` implements `ResetInterface`
* marked all dispatched event classes as `@final`
* added support for displaying table horizontally

View File

@ -256,14 +256,14 @@ final class ProgressBar
$this->redrawFreq = null !== $freq ? max(1, $freq) : null;
}
public function preventRedrawFasterThan(float $intervalInSeconds): void
public function minSecondsBetweenRedraws(float $seconds): void
{
$this->minSecondsBetweenRedraws = $intervalInSeconds;
$this->minSecondsBetweenRedraws = $seconds;
}
public function forceRedrawSlowerThan(float $intervalInSeconds): void
public function maxSecondsBetweenRedraws(float $seconds): void
{
$this->maxSecondsBetweenRedraws = $intervalInSeconds;
$this->maxSecondsBetweenRedraws = $seconds;
}
/**

View File

@ -79,7 +79,9 @@ class Terminal
// or [w, h] from "wxh"
self::$width = (int) $matches[1];
self::$height = isset($matches[4]) ? (int) $matches[4] : (int) $matches[2];
} elseif (self::hasSttyAvailable()) {
} elseif (!self::hasVt100Support() && self::hasSttyAvailable()) {
// only use stty on Windows if the terminal does not support vt100 (e.g. Windows 7 + git-bash)
// testing for stty in a Windows 10 vt100-enabled console will implicitly disable vt100 support on STDOUT
self::initDimensionsUsingStty();
} elseif (null !== $dimensions = self::getConsoleMode()) {
// extract [w, h] from "wxh"
@ -91,6 +93,17 @@ class Terminal
}
}
/**
* Returns whether STDOUT has vt100 support (some Windows 10+ configurations).
*/
private static function hasVt100Support(): bool
{
return \function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(STDOUT);
}
/**
* Initializes dimensions using the output of an stty columns line.
*/
private static function initDimensionsUsingStty()
{
if ($sttyString = self::getSttyColumns()) {

View File

@ -936,23 +936,47 @@ class ProgressBarTest extends TestCase
putenv('COLUMNS=120');
}
public function testForceRedrawSlowerThan(): void
public function testMinAndMaxSecondsBetweenRedraws(): void
{
$bar = new ProgressBar($output = $this->getOutputStream());
$bar->setRedrawFrequency(1);
$bar->minSecondsBetweenRedraws(5);
$bar->maxSecondsBetweenRedraws(10);
$bar->start();
$bar->setProgress(1);
sleep(10);
$bar->setProgress(2);
sleep(20);
$bar->setProgress(3);
rewind($output->getStream());
$this->assertEquals(
' 0 [>---------------------------]'.
$this->generateOutput(' 2 [-->-------------------------]').
$this->generateOutput(' 3 [--->------------------------]'),
stream_get_contents($output->getStream())
);
}
public function testMaxSecondsBetweenRedraws(): void
{
$bar = new ProgressBar($output = $this->getOutputStream(), 0, 0);
$bar->setRedrawFrequency(4); // disable step based redraws
$bar->start();
$bar->setProgress(1); // No treshold hit, no redraw
$bar->forceRedrawSlowerThan(2);
$bar->maxSecondsBetweenRedraws(2);
sleep(1);
$bar->setProgress(2); // Still no redraw because redraw is forced after 2 seconds only
$bar->setProgress(2); // Still no redraw because it takes 2 seconds for a redraw
sleep(1);
$bar->setProgress(3); // 1+1 = 2 -> redraw finally
$bar->setProgress(4); // step based redraw freq hit, redraw even without sleep
$bar->setProgress(5); // No treshold hit, no redraw
$bar->preventRedrawFasterThan(3);
$bar->maxSecondsBetweenRedraws(3);
sleep(2);
$bar->setProgress(6); // No redraw even though 2 seconds passed. Throttling has priority
$bar->preventRedrawFasterThan(2);
$bar->maxSecondsBetweenRedraws(2);
$bar->setProgress(7); // Throttling relaxed, draw
rewind($output->getStream());
@ -965,16 +989,16 @@ class ProgressBarTest extends TestCase
);
}
public function testPreventRedrawFasterThan()
public function testMinSecondsBetweenRedraws()
{
$bar = new ProgressBar($output = $this->getOutputStream(), 0, 0);
$bar->setRedrawFrequency(1);
$bar->preventRedrawFasterThan(1);
$bar->minSecondsBetweenRedraws(1);
$bar->start();
$bar->setProgress(1); // Too fast, should not draw
sleep(1);
$bar->setProgress(2); // 1 second passed, draw
$bar->preventRedrawFasterThan(2);
$bar->minSecondsBetweenRedraws(2);
sleep(1);
$bar->setProgress(3); // 1 second passed but we changed threshold, should not draw
sleep(1);

View File

@ -16,6 +16,7 @@ CHANGELOG
4.4.0
-----
* added `CheckTypeDeclarationsPass` to check injected parameters type during compilation
* added support for opcache.preload by generating a preloading script in the cache folder
* added support for dumping the container in one file instead of many files
* deprecated support for short factories and short configurators in Yaml
@ -27,6 +28,7 @@ CHANGELOG
* added support for improved syntax to define method calls in Yaml
* added `LazyString` for lazy computation of string values injected into services
* made the `%env(base64:...)%` processor able to decode base64url
* added ability to choose behavior of decorations on non existent decorated services
4.3.0
-----

View File

@ -130,9 +130,12 @@ abstract class AbstractRecursivePass implements CompilerPassInterface
list($class, $method) = $factory;
if ($class instanceof Reference) {
$class = $this->container->findDefinition((string) $class)->getClass();
} elseif ($class instanceof Definition) {
$class = $class->getClass();
} elseif (null === $class) {
$class = $definition->getClass();
}
if ('__construct' === $method) {
throw new RuntimeException(sprintf('Invalid service "%s": "__construct()" cannot be used as a factory method.', $this->currentId));
}

View File

@ -0,0 +1,192 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
/**
* Checks whether injected parameters are compatible with type declarations.
*
* This pass should be run after all optimization passes.
*
* It can be added either:
* * before removing passes to check all services even if they are not currently used,
* * after removing passes to check only services are used in the app.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Julien Maulny <jmaulny@darkmira.fr>
*/
final class CheckTypeDeclarationsPass extends AbstractRecursivePass
{
private const SCALAR_TYPES = ['int', 'float', 'bool', 'string'];
private $autoload;
/**
* @param bool $autoload Whether services who's class in not loaded should be checked or not.
* Defaults to false to save loading code during compilation.
*/
public function __construct(bool $autoload = false)
{
$this->autoload = $autoload;
}
/**
* {@inheritdoc}
*/
protected function processValue($value, $isRoot = false)
{
if (!$value instanceof Definition) {
return parent::processValue($value, $isRoot);
}
if (!$this->autoload && !class_exists($class = $value->getClass(), false) && !interface_exists($class, false)) {
return parent::processValue($value, $isRoot);
}
if (ServiceLocator::class === $value->getClass()) {
return parent::processValue($value, $isRoot);
}
if ($constructor = $this->getConstructor($value, false)) {
$this->checkTypeDeclarations($value, $constructor, $value->getArguments());
}
foreach ($value->getMethodCalls() as $methodCall) {
$reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]);
$this->checkTypeDeclarations($value, $reflectionMethod, $methodCall[1]);
}
return parent::processValue($value, $isRoot);
}
/**
* @throws InvalidArgumentException When not enough parameters are defined for the method
*/
private function checkTypeDeclarations(Definition $checkedDefinition, \ReflectionFunctionAbstract $reflectionFunction, array $values): void
{
$numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters();
if (\count($values) < $numberOfRequiredParameters) {
throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, \count($values)));
}
$reflectionParameters = $reflectionFunction->getParameters();
$checksCount = min($reflectionFunction->getNumberOfParameters(), \count($values));
for ($i = 0; $i < $checksCount; ++$i) {
if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) {
continue;
}
$this->checkType($checkedDefinition, $values[$i], $reflectionParameters[$i]);
}
if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
$variadicParameters = \array_slice($values, $lastParameter->getPosition());
foreach ($variadicParameters as $variadicParameter) {
$this->checkType($checkedDefinition, $variadicParameter, $lastParameter);
}
}
}
/**
* @throws InvalidParameterTypeException When a parameter is not compatible with the declared type
*/
private function checkType(Definition $checkedDefinition, $value, \ReflectionParameter $parameter): void
{
$type = $parameter->getType()->getName();
if ($value instanceof Reference) {
if (!$this->container->has($value = (string) $value)) {
return;
}
if ('service_container' === $value && is_a($type, Container::class, true)) {
return;
}
$value = $this->container->findDefinition($value);
}
if ('self' === $type) {
$type = $parameter->getDeclaringClass()->getName();
}
if ('static' === $type) {
$type = $checkedDefinition->getClass();
}
if ($value instanceof Definition) {
$class = $value->getClass();
if (!$class || (!$this->autoload && !class_exists($class, false) && !interface_exists($class, false))) {
return;
}
if ('callable' === $type && method_exists($class, '__invoke')) {
return;
}
if ('iterable' === $type && is_subclass_of($class, 'Traversable')) {
return;
}
if (is_a($class, $type, true)) {
return;
}
throw new InvalidParameterTypeException($this->currentId, $class, $parameter);
}
if ($value instanceof Parameter) {
$value = $this->container->getParameter($value);
} elseif (\is_string($value) && '%' === ($value[0] ?? '') && preg_match('/^%([^%]+)%$/', $value, $match)) {
$value = $this->container->getParameter($match[1]);
}
if (null === $value && $parameter->allowsNull()) {
return;
}
if (\in_array($type, self::SCALAR_TYPES, true) && is_scalar($value)) {
return;
}
if ('callable' === $type && \is_array($value) && isset($value[0]) && ($value[0] instanceof Reference || $value[0] instanceof Definition)) {
return;
}
if ('iterable' === $type && (\is_array($value) || $value instanceof \Traversable || $value instanceof IteratorArgument)) {
return;
}
if ('Traversable' === $type && ($value instanceof \Traversable || $value instanceof IteratorArgument)) {
return;
}
$checkFunction = sprintf('is_%s', $parameter->getType()->getName());
if (!$parameter->getType()->isBuiltin() || !$checkFunction($value)) {
throw new InvalidParameterTypeException($this->currentId, \gettype($value), $parameter);
}
}
}

View File

@ -13,6 +13,9 @@ namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Reference;
/**
* Overwrites a service but keeps the overridden one.
@ -37,7 +40,9 @@ class DecoratorServicePass implements CompilerPassInterface
$decoratingDefinitions = [];
foreach ($definitions as list($id, $definition)) {
list($inner, $renamedId) = $definition->getDecoratedService();
$decoratedService = $definition->getDecoratedService();
list($inner, $renamedId) = $decoratedService;
$invalidBehavior = $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
$definition->setDecoratedService(null);
@ -45,6 +50,7 @@ class DecoratorServicePass implements CompilerPassInterface
$renamedId = $id.'.inner';
}
$definition->innerServiceId = $renamedId;
$definition->decorationOnInvalid = $invalidBehavior;
// we create a new alias/service for the service we are replacing
// to be able to reference it in the new one
@ -53,13 +59,21 @@ class DecoratorServicePass implements CompilerPassInterface
$public = $alias->isPublic();
$private = $alias->isPrivate();
$container->setAlias($renamedId, new Alias((string) $alias, false));
} else {
} elseif ($container->hasDefinition($inner)) {
$decoratedDefinition = $container->getDefinition($inner);
$public = $decoratedDefinition->isPublic();
$private = $decoratedDefinition->isPrivate();
$decoratedDefinition->setPublic(false);
$container->setDefinition($renamedId, $decoratedDefinition);
$decoratingDefinitions[$inner] = $decoratedDefinition;
} elseif (ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $invalidBehavior) {
$container->removeDefinition($id);
continue;
} elseif (ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) {
$public = $definition->isPublic();
$private = $definition->isPrivate();
} else {
throw new ServiceNotFoundException($inner, $id);
}
if (isset($decoratingDefinitions[$inner])) {

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ExceptionInterface;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@ -149,7 +150,7 @@ class ResolveChildDefinitionsPass extends AbstractRecursivePass
if (null === $decoratedService) {
$def->setDecoratedService($decoratedService);
} else {
$def->setDecoratedService($decoratedService[0], $decoratedService[1], $decoratedService[2]);
$def->setDecoratedService($decoratedService[0], $decoratedService[1], $decoratedService[2], $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);
}
}

View File

@ -42,7 +42,9 @@ class ResolveInvalidReferencesPass implements CompilerPassInterface
$this->signalingException = new RuntimeException('Invalid reference.');
try {
$this->processValue($container->getDefinitions(), 1);
foreach ($container->getDefinitions() as $this->currentId => $definition) {
$this->processValue($definition);
}
} finally {
$this->container = $this->signalingException = null;
}
@ -72,9 +74,6 @@ class ResolveInvalidReferencesPass implements CompilerPassInterface
$i = 0;
foreach ($value as $k => $v) {
if (!$rootLevel) {
$this->currentId = $k;
}
try {
if (false !== $i && $k !== $i++) {
$i = false;
@ -101,6 +100,14 @@ class ResolveInvalidReferencesPass implements CompilerPassInterface
if ($this->container->has($id = (string) $value)) {
return $value;
}
$currentDefinition = $this->container->getDefinition($this->currentId);
// resolve decorated service behavior depending on decorator service
if ($currentDefinition->innerServiceId === $id && ContainerInterface::NULL_ON_INVALID_REFERENCE === $currentDefinition->decorationOnInvalid) {
return null;
}
$invalidBehavior = $value->getInvalidBehavior();
if (ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $invalidBehavior && $value instanceof TypedReference && !$this->container->has($id)) {

View File

@ -56,6 +56,13 @@ class Definition
*/
public $innerServiceId;
/**
* @internal
*
* Used to store the behavior to follow when using service decoration and the decorated service is invalid
*/
public $decorationOnInvalid;
public function __construct(string $class = null, array $arguments = [])
{
if (null !== $class) {
@ -125,13 +132,12 @@ class Definition
*
* @param string|null $id The decorated service id, use null to remove decoration
* @param string|null $renamedId The new decorated service id
* @param int $priority The priority of decoration
*
* @return $this
*
* @throws InvalidArgumentException in case the decorated service id and the new decorated service id are equals
*/
public function setDecoratedService(?string $id, ?string $renamedId = null, int $priority = 0)
public function setDecoratedService(?string $id, ?string $renamedId = null, int $priority = 0, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE)
{
if ($renamedId && $id === $renamedId) {
throw new InvalidArgumentException(sprintf('The decorated service inner name for "%s" must be different than the service name itself.', $id));
@ -143,6 +149,10 @@ class Definition
$this->decoratedService = null;
} else {
$this->decoratedService = [$id, $renamedId, (int) $priority];
if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) {
$this->decoratedService[] = $invalidBehavior;
}
}
return $this;

View File

@ -116,9 +116,15 @@ class XmlDumper extends Dumper
if ($definition->isLazy()) {
$service->setAttribute('lazy', 'true');
}
if (null !== $decorated = $definition->getDecoratedService()) {
list($decorated, $renamedId, $priority) = $decorated;
if (null !== $decoratedService = $definition->getDecoratedService()) {
list($decorated, $renamedId, $priority) = $decoratedService;
$service->setAttribute('decorates', $decorated);
$decorationOnInvalid = $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
if (\in_array($decorationOnInvalid, [ContainerInterface::IGNORE_ON_INVALID_REFERENCE, ContainerInterface::NULL_ON_INVALID_REFERENCE], true)) {
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE === $decorationOnInvalid ? 'null' : 'ignore';
$service->setAttribute('decoration-on-invalid', $invalidBehavior);
}
if (null !== $renamedId) {
$service->setAttribute('decoration-inner-name', $renamedId);
}

View File

@ -131,8 +131,8 @@ class YamlDumper extends Dumper
$code .= " shared: false\n";
}
if (null !== $decorated = $definition->getDecoratedService()) {
list($decorated, $renamedId, $priority) = $decorated;
if (null !== $decoratedService = $definition->getDecoratedService()) {
list($decorated, $renamedId, $priority) = $decoratedService;
$code .= sprintf(" decorates: %s\n", $decorated);
if (null !== $renamedId) {
$code .= sprintf(" decoration_inner_name: %s\n", $renamedId);
@ -140,6 +140,12 @@ class YamlDumper extends Dumper
if (0 !== $priority) {
$code .= sprintf(" decoration_priority: %s\n", $priority);
}
$decorationOnInvalid = $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
if (\in_array($decorationOnInvalid, [ContainerInterface::IGNORE_ON_INVALID_REFERENCE, ContainerInterface::NULL_ON_INVALID_REFERENCE])) {
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE === $decorationOnInvalid ? 'null' : 'ignore';
$code .= sprintf(" decoration_on_invalid: %s\n", $invalidBehavior);
}
}
if ($callable = $definition->getFactory()) {

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Exception;
/**
* Thrown when trying to inject a parameter into a constructor/method with an incompatible type.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Julien Maulny <jmaulny@darkmira.fr>
*/
class InvalidParameterTypeException extends InvalidArgumentException
{
public function __construct(string $serviceId, string $type, \ReflectionParameter $parameter)
{
parent::__construct(sprintf('Invalid definition for service "%s": argument %d of "%s::%s" accepts "%s", "%s" passed.', $serviceId, 1 + $parameter->getPosition(), $parameter->getDeclaringClass()->getName(), $parameter->getDeclaringFunction()->getName(), $parameter->getType()->getName(), $type));
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
trait DecorateTrait
@ -24,9 +25,9 @@ trait DecorateTrait
*
* @throws InvalidArgumentException in case the decorated service id and the new decorated service id are equals
*/
final public function decorate(?string $id, string $renamedId = null, int $priority = 0): self
final public function decorate(?string $id, string $renamedId = null, int $priority = 0, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): self
{
$this->definition->setDecoratedService($id, $renamedId, $priority);
$this->definition->setDecoratedService($id, $renamedId, $priority, $invalidBehavior);
return $this;
}

View File

@ -362,10 +362,22 @@ class XmlFileLoader extends FileLoader
$definition->setBindings($bindings);
}
if ($value = $service->getAttribute('decorates')) {
if ($decorates = $service->getAttribute('decorates')) {
$decorationOnInvalid = $service->getAttribute('decoration-on-invalid') ?: 'exception';
if ('exception' === $decorationOnInvalid) {
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
} elseif ('ignore' === $decorationOnInvalid) {
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
} elseif ('null' === $decorationOnInvalid) {
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
} else {
throw new InvalidArgumentException(sprintf('Invalid value "%s" for attribute "decoration-on-invalid" on service "%s". Did you mean "exception", "ignore" or "null" in "%s"?', $decorationOnInvalid, (string) $service->getAttribute('id'), $file));
}
$renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null;
$priority = $service->hasAttribute('decoration-priority') ? $service->getAttribute('decoration-priority') : 0;
$definition->setDecoratedService($value, $renameId, $priority);
$definition->setDecoratedService($decorates, $renameId, $priority, $invalidBehavior);
}
return $definition;

View File

@ -58,6 +58,7 @@ class YamlFileLoader extends FileLoader
'decorates' => 'decorates',
'decoration_inner_name' => 'decoration_inner_name',
'decoration_priority' => 'decoration_priority',
'decoration_on_invalid' => 'decoration_on_invalid',
'autowire' => 'autowire',
'autoconfigure' => 'autoconfigure',
'bind' => 'bind',
@ -538,14 +539,28 @@ class YamlFileLoader extends FileLoader
$definition->addTag($name, $tag);
}
if (isset($service['decorates'])) {
if ('' !== $service['decorates'] && '@' === $service['decorates'][0]) {
throw new InvalidArgumentException(sprintf('The value of the "decorates" option for the "%s" service must be the id of the service without the "@" prefix (replace "%s" with "%s").', $id, $service['decorates'], substr($service['decorates'], 1)));
if (null !== $decorates = $service['decorates'] ?? null) {
if ('' !== $decorates && '@' === $decorates[0]) {
throw new InvalidArgumentException(sprintf('The value of the "decorates" option for the "%s" service must be the id of the service without the "@" prefix (replace "%s" with "%s").', $id, $service['decorates'], substr($decorates, 1)));
}
$decorationOnInvalid = \array_key_exists('decoration_on_invalid', $service) ? $service['decoration_on_invalid'] : 'exception';
if ('exception' === $decorationOnInvalid) {
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
} elseif ('ignore' === $decorationOnInvalid) {
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
} elseif (null === $decorationOnInvalid) {
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
} elseif ('null' === $decorationOnInvalid) {
throw new InvalidArgumentException(sprintf('Invalid value "%s" for attribute "decoration_on_invalid" on service "%s". Did you mean null (without quotes) in "%s"?', $decorationOnInvalid, $id, $file));
} else {
throw new InvalidArgumentException(sprintf('Invalid value "%s" for attribute "decoration_on_invalid" on service "%s". Did you mean "exception", "ignore" or null in "%s"?', $decorationOnInvalid, $id, $file));
}
$renameId = isset($service['decoration_inner_name']) ? $service['decoration_inner_name'] : null;
$priority = isset($service['decoration_priority']) ? $service['decoration_priority'] : 0;
$definition->setDecoratedService($service['decorates'], $renameId, $priority);
$definition->setDecoratedService($decorates, $renameId, $priority, $invalidBehavior);
}
if (isset($service['autowire'])) {

View File

@ -129,6 +129,7 @@
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="parent" type="xsd:string" />
<xsd:attribute name="decorates" type="xsd:string" />
<xsd:attribute name="decoration-on-invalid" type="invalid_decorated_service_sequence" />
<xsd:attribute name="decoration-inner-name" type="xsd:string" />
<xsd:attribute name="decoration-priority" type="xsd:integer" />
<xsd:attribute name="autowire" type="boolean" />
@ -281,6 +282,14 @@
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="invalid_decorated_service_sequence">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="null" />
<xsd:enumeration value="ignore" />
<xsd:enumeration value="exception" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="boolean">
<xsd:restriction base="xsd:string">
<xsd:pattern value="(%.+%|true|false)" />

View File

@ -0,0 +1,555 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Bar;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgument;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgumentNotNull;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Foo;
/**
* @author Nicolas Grekas <p@tchwork.com>
* @author Julien Maulny <jmaulny@darkmira.fr>
*/
class CheckTypeDeclarationsPassTest extends TestCase
{
public function testProcessThrowsExceptionOnInvalidTypesConstructorArguments()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.');
$container = new ContainerBuilder();
$container->register('foo', Foo::class);
$container->register('bar', Bar::class)
->addArgument(new Reference('foo'));
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessThrowsExceptionOnInvalidTypesMethodCallArguments()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoo" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.');
$container = new ContainerBuilder();
$container->register('foo', Foo::class);
$container->register('bar', BarMethodCall::class)
->addMethodCall('setFoo', [new Reference('foo')]);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessFailsWhenPassingNullToRequiredArgument()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct" accepts "stdClass", "NULL" passed.');
$container = new ContainerBuilder();
$container->register('bar', Bar::class)
->addArgument(null);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessThrowsExceptionWhenMissingArgumentsInConstructor()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct()" requires 1 arguments, 0 passed.');
$container = new ContainerBuilder();
$container->register('bar', Bar::class);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessSuccessWhenPassingTooManyArgumentInConstructor()
{
$container = new ContainerBuilder();
$container->register('foo', \stdClass::class);
$container->register('bar', Bar::class)
->addArgument(new Reference('foo'))
->addArgument(new Reference('foo'));
(new CheckTypeDeclarationsPass(true))->process($container);
$this->addToAssertionCount(1);
}
public function testProcessRegisterWithClassName()
{
$container = new ContainerBuilder();
$container->register(Foo::class, Foo::class);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(Foo::class, $container->get(Foo::class));
}
public function testProcessThrowsExceptionWhenMissingArgumentsInMethodCall()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoo()" requires 1 arguments, 0 passed.');
$container = new ContainerBuilder();
$container->register('foo', \stdClass::class);
$container->register('bar', BarMethodCall::class)
->addArgument(new Reference('foo'))
->addMethodCall('setFoo', []);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessVariadicFails()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 2 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoosVariadic" accepts "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo", "stdClass" passed.');
$container = new ContainerBuilder();
$container->register('stdClass', \stdClass::class);
$container->register('foo', Foo::class);
$container->register('bar', BarMethodCall::class)
->addMethodCall('setFoosVariadic', [
new Reference('foo'),
new Reference('foo'),
new Reference('stdClass'),
]);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessVariadicFailsOnPassingBadTypeOnAnotherArgument()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoosVariadic" accepts "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo", "stdClass" passed.');
$container = new ContainerBuilder();
$container->register('stdClass', \stdClass::class);
$container->register('bar', BarMethodCall::class)
->addMethodCall('setFoosVariadic', [
new Reference('stdClass'),
]);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessVariadicSuccess()
{
$container = new ContainerBuilder();
$container->register('foo', Foo::class);
$container->register('bar', BarMethodCall::class)
->addMethodCall('setFoosVariadic', [
new Reference('foo'),
new Reference('foo'),
new Reference('foo'),
]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(Foo::class, $container->get('bar')->foo);
}
public function testProcessSuccessWhenNotUsingOptionalArgument()
{
$container = new ContainerBuilder();
$container->register('foo', Foo::class);
$container->register('bar', BarMethodCall::class)
->addMethodCall('setFoosOptional', [
new Reference('foo'),
]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(Foo::class, $container->get('bar')->foo);
}
public function testProcessSuccessWhenUsingOptionalArgumentWithGoodType()
{
$container = new ContainerBuilder();
$container->register('foo', Foo::class);
$container->register('bar', BarMethodCall::class)
->addMethodCall('setFoosOptional', [
new Reference('foo'),
new Reference('foo'),
]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(Foo::class, $container->get('bar')->foo);
}
public function testProcessFailsWhenUsingOptionalArgumentWithBadType()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 2 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoosOptional" accepts "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo", "stdClass" passed.');
$container = new ContainerBuilder();
$container->register('stdClass', \stdClass::class);
$container->register('foo', Foo::class);
$container->register('bar', BarMethodCall::class)
->addMethodCall('setFoosOptional', [
new Reference('foo'),
new Reference('stdClass'),
]);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessSuccessWhenPassingNullToOptional()
{
$container = new ContainerBuilder();
$container->register('bar', BarOptionalArgument::class)
->addArgument(null);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertNull($container->get('bar')->foo);
}
public function testProcessSuccessWhenPassingNullToOptionalThatDoesNotAcceptNull()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarOptionalArgumentNotNull::__construct" accepts "int", "NULL" passed.');
$container = new ContainerBuilder();
$container->register('bar', BarOptionalArgumentNotNull::class)
->addArgument(null);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessFailsWhenPassingBadTypeToOptional()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarOptionalArgument::__construct" accepts "stdClass", "string" passed.');
$container = new ContainerBuilder();
$container->register('bar', BarOptionalArgument::class)
->addArgument('string instead of stdClass');
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertNull($container->get('bar')->foo);
}
public function testProcessSuccessScalarType()
{
$container = new ContainerBuilder();
$container->register('bar', BarMethodCall::class)
->addMethodCall('setScalars', [
1,
'string',
]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(BarMethodCall::class, $container->get('bar'));
}
public function testProcessFailsOnPassingScalarTypeToConstructorTypedWithClass()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct" accepts "stdClass", "integer" passed.');
$container = new ContainerBuilder();
$container->register('bar', Bar::class)
->addArgument(1);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessFailsOnPassingScalarTypeToMethodTypedWithClass()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoo" accepts "stdClass", "string" passed.');
$container = new ContainerBuilder();
$container->register('bar', BarMethodCall::class)
->addMethodCall('setFoo', [
'builtin type instead of class',
]);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessFailsOnPassingClassToScalarTypedParameter()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setScalars" accepts "int", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.');
$container = new ContainerBuilder();
$container->register('foo', Foo::class);
$container->register('bar', BarMethodCall::class)
->addMethodCall('setScalars', [
new Reference('foo'),
new Reference('foo'),
]);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessSuccessOnPassingBadScalarType()
{
$container = new ContainerBuilder();
$container->register('bar', BarMethodCall::class)
->addMethodCall('setScalars', [
1,
true,
]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(BarMethodCall::class, $container->get('bar'));
}
public function testProcessSuccessPassingBadScalarTypeOptionalArgument()
{
$container = new ContainerBuilder();
$container->register('bar', BarMethodCall::class)
->addMethodCall('setScalars', [
1,
'string',
'string instead of optional boolean',
]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(BarMethodCall::class, $container->get('bar'));
}
public function testProcessSuccessWhenPassingArray()
{
$container = new ContainerBuilder();
$container->register('bar', BarMethodCall::class)
->addMethodCall('setArray', [[]]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(BarMethodCall::class, $container->get('bar'));
}
public function testProcessSuccessWhenPassingIntegerToArrayTypedParameter()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall::setArray" accepts "array", "integer" passed.');
$container = new ContainerBuilder();
$container->register('bar', BarMethodCall::class)
->addMethodCall('setArray', [1]);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessSuccessWhenPassingAnIteratorArgumentToIterable()
{
$container = new ContainerBuilder();
$container->register('bar', BarMethodCall::class)
->addMethodCall('setIterable', [new IteratorArgument([])]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->addToAssertionCount(1);
}
public function testProcessFactory()
{
$container = new ContainerBuilder();
$container->register('foo', Foo::class);
$container->register('bar', Bar::class)
->setFactory([
new Reference('foo'),
'createBar',
]);
/* Asserts that the class of Bar is well detected */
$container->register('bar_call', BarMethodCall::class)
->addMethodCall('setBar', [new Reference('bar')]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(Bar::class, $container->get('bar'));
}
public function testProcessFactoryFailsOnInvalidParameterType()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo::createBarArguments" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.');
$container = new ContainerBuilder();
$container->register('foo', Foo::class);
$container->register('bar', Bar::class)
->addArgument(new Reference('foo'))
->setFactory([
new Reference('foo'),
'createBarArguments',
]);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessFactoryFailsOnInvalidParameterTypeOptional()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar": argument 2 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo::createBarArguments" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.');
$container = new ContainerBuilder();
$container->register('stdClass', \stdClass::class);
$container->register('foo', Foo::class);
$container->register('bar', Bar::class)
->addArgument(new Reference('stdClass'))
->addArgument(new Reference('foo'))
->setFactory([
new Reference('foo'),
'createBarArguments',
]);
(new CheckTypeDeclarationsPass(true))->process($container);
}
public function testProcessFactorySuccessOnValidTypes()
{
$container = new ContainerBuilder();
$container->register('stdClass', \stdClass::class);
$container->register('foo', Foo::class);
$container->register('bar', Bar::class)
->addArgument(new Reference('stdClass'))
->addArgument(new Reference('stdClass'))
->setFactory([
new Reference('foo'),
'createBarArguments',
]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->addToAssertionCount(1);
}
public function testProcessFactoryCallbackSuccessOnValidType()
{
$container = new ContainerBuilder();
$container->register('bar', \DateTime::class)
->setFactory('date_create');
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(\DateTime::class, $container->get('bar'));
}
public function testProcessDoesNotLoadCodeByDefault()
{
$container = new ContainerBuilder();
$container->register('foo', FooNotExisting::class);
$container->register('bar', BarNotExisting::class)
->addArgument(new Reference('foo'))
->addMethodCall('setFoo', [
new Reference('foo'),
'string',
1,
]);
(new CheckTypeDeclarationsPass())->process($container);
$this->addToAssertionCount(1);
}
public function testProcessFactoryDoesNotLoadCodeByDefault()
{
$container = new ContainerBuilder();
$container->register('foo', FooNotExisting::class);
$container->register('bar', BarNotExisting::class)
->setFactory([
new Reference('foo'),
'notExistingMethod',
]);
(new CheckTypeDeclarationsPass())->process($container);
$this->addToAssertionCount(1);
}
public function testProcessPassingBuiltinTypeDoesNotLoadCodeByDefault()
{
$container = new ContainerBuilder();
$container->register('bar', BarNotExisting::class)
->addArgument(1);
(new CheckTypeDeclarationsPass())->process($container);
$this->addToAssertionCount(1);
}
public function testProcessDoesNotThrowsExceptionOnValidTypes()
{
$container = new ContainerBuilder();
$container->register('foo', \stdClass::class);
$container->register('bar', Bar::class)
->addArgument(new Reference('foo'));
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(\stdClass::class, $container->get('bar')->foo);
}
public function testProcessThrowsOnIterableTypeWhenScalarPassed()
{
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid definition for service "bar_call": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setIterable" accepts "iterable", "integer" passed.');
$container = new ContainerBuilder();
$container->register('bar_call', BarMethodCall::class)
->addMethodCall('setIterable', [2]);
(new CheckTypeDeclarationsPass(true))->process($container);
$this->assertInstanceOf(\stdClass::class, $container->get('bar')->foo);
}
}

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
class DecoratorServicePassTest extends TestCase
{
@ -125,6 +126,61 @@ class DecoratorServicePassTest extends TestCase
$this->assertNull($quxDefinition->getDecoratedService());
}
public function testProcessWithInvalidDecorated()
{
$container = new ContainerBuilder();
$decoratorDefinition = $container
->register('decorator')
->setDecoratedService('unknown_decorated', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)
;
$this->process($container);
$this->assertFalse($container->has('decorator'));
$container = new ContainerBuilder();
$decoratorDefinition = $container
->register('decorator')
->setDecoratedService('unknown_decorated', null, 0, ContainerInterface::NULL_ON_INVALID_REFERENCE)
;
$this->process($container);
$this->assertTrue($container->has('decorator'));
$this->assertSame(ContainerInterface::NULL_ON_INVALID_REFERENCE, $decoratorDefinition->decorationOnInvalid);
$container = new ContainerBuilder();
$decoratorDefinition = $container
->register('decorator')
->setDecoratedService('unknown_service')
;
$this->expectException('Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException');
$this->process($container);
}
public function testProcessNoInnerAliasWithInvalidDecorated()
{
$container = new ContainerBuilder();
$decoratorDefinition = $container
->register('decorator')
->setDecoratedService('unknown_decorated', null, 0, ContainerInterface::NULL_ON_INVALID_REFERENCE)
;
$this->process($container);
$this->assertFalse($container->hasAlias('decorator.inner'));
}
public function testProcessWithInvalidDecoratedAndWrongBehavior()
{
$container = new ContainerBuilder();
$decoratorDefinition = $container
->register('decorator')
->setDecoratedService('unknown_decorated', null, 0, 12)
;
$this->expectException('Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException');
$this->process($container);
}
public function testProcessMovesTagsFromDecoratedDefinitionToDecoratingDefinition()
{
$container = new ContainerBuilder();

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass;
use Symfony\Component\DependencyInjection\Compiler\ResolveInvalidReferencesPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -127,6 +128,43 @@ class ResolveInvalidReferencesPassTest extends TestCase
$this->assertSame([[[]]], $def->getArguments());
}
public function testProcessSetDecoratedAsNullOnInvalid()
{
$container = new ContainerBuilder();
$decoratorDefinition = $container
->register('decorator')
->setArguments([
new Reference('decorator.inner'),
])
->setDecoratedService('unknown_decorated', null, 0, ContainerInterface::NULL_ON_INVALID_REFERENCE)
;
(new DecoratorServicePass())->process($container);
(new ResolveInvalidReferencesPass())->process($container);
$this->assertSame([null], $decoratorDefinition->getArguments());
}
public function testProcessSetOnlyDecoratedAsNullOnInvalid()
{
$container = new ContainerBuilder();
$unknownArgument = new Reference('unknown_argument');
$decoratorDefinition = $container
->register('decorator')
->setArguments([
new Reference('decorator.inner'),
$unknownArgument,
])
->setDecoratedService('unknown_decorated', null, 0, ContainerInterface::NULL_ON_INVALID_REFERENCE)
;
(new DecoratorServicePass())->process($container);
(new ResolveInvalidReferencesPass())->process($container);
$this->assertNull($decoratorDefinition->getArguments()[0]);
$this->assertEquals($unknownArgument, $decoratorDefinition->getArguments()[1]);
}
protected function process(ContainerBuilder $container)
{
$pass = new ResolveInvalidReferencesPass();

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
@ -51,6 +52,13 @@ class DefinitionTest extends TestCase
public function testSetGetDecoratedService()
{
$def = new Definition('stdClass');
$this->assertNull($def->getDecoratedService());
$def->setDecoratedService('foo', 'foo.renamed', 5, ContainerInterface::NULL_ON_INVALID_REFERENCE);
$this->assertEquals(['foo', 'foo.renamed', 5, ContainerInterface::NULL_ON_INVALID_REFERENCE], $def->getDecoratedService());
$def->setDecoratedService(null);
$this->assertNull($def->getDecoratedService());
$def = new Definition('stdClass');
$this->assertNull($def->getDecoratedService());
$def->setDecoratedService('foo', 'foo.renamed', 5);

View File

@ -146,6 +146,16 @@ class XmlDumperTest extends TestCase
</services>
</container>
", include $fixturesPath.'/containers/container16.php'],
["<?xml version=\"1.0\" encoding=\"utf-8\"?>
<container xmlns=\"http://symfony.com/schema/dic/services\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd\">
<services>
<service id=\"service_container\" class=\"Symfony\Component\DependencyInjection\ContainerInterface\" public=\"true\" synthetic=\"true\"/>
<service id=\"decorator\" decorates=\"decorated\" decoration-on-invalid=\"null\" decoration-inner-name=\"decorated.inner\" decoration-priority=\"1\"/>
<service id=\"Psr\Container\ContainerInterface\" alias=\"service_container\" public=\"false\"/>
<service id=\"Symfony\Component\DependencyInjection\ContainerInterface\" alias=\"service_container\" public=\"false\"/>
</services>
</container>
", include $fixturesPath.'/containers/container34.php'],
];
}

View File

@ -71,6 +71,13 @@ class YamlDumperTest extends TestCase
$this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services24.yml', $dumper->dump());
}
public function testDumpDecoratedServices()
{
$container = include self::$fixturesPath.'/containers/container34.php';
$dumper = new YamlDumper($container);
$this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services34.yml', $dumper->dump());
}
public function testDumpLoad()
{
$container = new ContainerBuilder();

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass;
class Bar
{
public $foo;
public function __construct(\stdClass $foo)
{
$this->foo = $foo;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass;
class BarMethodCall
{
public $foo;
public function setBar(Bar $bar)
{
}
public function setFoo(\stdClass $foo)
{
$this->foo = $foo;
}
public function setFoosVariadic(Foo $foo, Foo ...$foos)
{
$this->foo = $foo;
}
public function setFoosOptional(Foo $foo, Foo $fooOptional = null)
{
$this->foo = $foo;
}
public function setScalars(int $int, string $string, bool $bool = false)
{
}
public function setArray(array $array)
{
}
public function setIterable(iterable $iterable)
{
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass;
class BarOptionalArgument
{
public $foo;
public function __construct(\stdClass $foo = null)
{
$this->foo = $foo;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass;
class BarOptionalArgumentNotNull
{
public $foo;
public function __construct(int $foo = 1)
{
$this->foo = $foo;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass;
class Foo
{
public static function createBar()
{
return new Bar(new \stdClass());
}
public static function createBarArguments(\stdClass $stdClass, \stdClass $stdClassOptional = null)
{
return new Bar($stdClass);
}
}

View File

@ -0,0 +1,12 @@
<?php
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
$container = new ContainerBuilder();
$container
->register('decorator')
->setDecoratedService('decorated', 'decorated.inner', 1, ContainerInterface::NULL_ON_INVALID_REFERENCE)
;
return $container;

View File

@ -47,7 +47,7 @@ class Symfony_DI_PhpDumper_Test_Rot13Parameters extends Container
public function getRemovedIds(): array
{
return [
'.service_locator.GU08LT9' => true,
'.service_locator.ZZqL6HL' => true,
'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
];

View File

@ -48,7 +48,7 @@ class Symfony_DI_PhpDumper_Service_Locator_Argument extends Container
public function getRemovedIds(): array
{
return [
'.service_locator.38dy3OH' => true,
'.service_locator.iSxuxv5' => true,
'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
'foo2' => true,

View File

@ -45,8 +45,8 @@ class ProjectServiceContainer extends Container
public function getRemovedIds(): array
{
return [
'.service_locator.nZQiwdg' => true,
'.service_locator.nZQiwdg.foo_service' => true,
'.service_locator.bPEFRiK' => true,
'.service_locator.bPEFRiK.foo_service' => true,
'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,

View File

@ -47,6 +47,7 @@
<service id="decorator_service" decorates="decorated" />
<service id="decorator_service_with_name" decorates="decorated" decoration-inner-name="decorated.pif-pouf"/>
<service id="decorator_service_with_name_and_priority" decorates="decorated" decoration-inner-name="decorated.pif-pouf" decoration-priority="5"/>
<service id="decorator_service_with_name_and_priority_and_on_invalid" decorates="decorated" decoration-inner-name="decorated.pif-pouf" decoration-priority="5" decoration-on-invalid="ignore"/>
<service id="new_factory1" class="FooBarClass">
<factory function="factory" />
</service>

View File

@ -0,0 +1,7 @@
services:
foo:
class: stdClass
bar:
class: stdClass
decorates: "foo"
decoration_on_invalid: 'null'

View File

@ -0,0 +1,17 @@
services:
service_container:
class: Symfony\Component\DependencyInjection\ContainerInterface
public: true
synthetic: true
decorator:
decorates: decorated
decoration_inner_name: decorated.inner
decoration_priority: 1
decoration_on_invalid: null
Psr\Container\ContainerInterface:
alias: service_container
public: false
Symfony\Component\DependencyInjection\ContainerInterface:
alias: service_container
public: false

View File

@ -30,6 +30,11 @@ services:
decorates: decorated
decoration_inner_name: decorated.pif-pouf
decoration_priority: 5
decorator_service_with_name_and_priority_and_on_invalid:
decorates: decorated
decoration_inner_name: decorated.pif-pouf
decoration_priority: 5
decoration_on_invalid: ignore
new_factory1: { class: FooBarClass, factory: factory}
new_factory2: { class: FooBarClass, factory: ['@baz', getClass]}
new_factory3: { class: FooBarClass, factory: [BazClass, getInstance]}

View File

@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator;
/**
* Tests for {@see \Symfony\Component\DependencyInjection\Instantiator\RealServiceInstantiator}.
* Tests for {@see \Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator}.
*
* @author Marco Pivetta <ocramius@gmail.com>
*/

View File

@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper;
/**
* Tests for {@see \Symfony\Component\DependencyInjection\PhpDumper\NullDumper}.
* Tests for {@see \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper}.
*
* @author Marco Pivetta <ocramius@gmail.com>
*/

View File

@ -38,7 +38,7 @@ class GlobFileLoaderTest extends TestCase
class GlobFileLoaderWithoutImport extends GlobFileLoader
{
public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null)
public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null, $exclude = null)
{
}
}

View File

@ -22,6 +22,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
@ -281,6 +282,7 @@ class XmlFileLoaderTest extends TestCase
$this->assertEquals(['decorated', null, 0], $services['decorator_service']->getDecoratedService());
$this->assertEquals(['decorated', 'decorated.pif-pouf', 0], $services['decorator_service_with_name']->getDecoratedService());
$this->assertEquals(['decorated', 'decorated.pif-pouf', 5], $services['decorator_service_with_name_and_priority']->getDecoratedService());
$this->assertEquals(['decorated', 'decorated.pif-pouf', 5, ContainerInterface::IGNORE_ON_INVALID_REFERENCE], $services['decorator_service_with_name_and_priority_and_on_invalid']->getDecoratedService());
}
public function testParsesIteratorArgument()

View File

@ -22,6 +22,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
@ -174,6 +175,7 @@ class YamlFileLoaderTest extends TestCase
$this->assertEquals(['decorated', null, 0], $services['decorator_service']->getDecoratedService());
$this->assertEquals(['decorated', 'decorated.pif-pouf', 0], $services['decorator_service_with_name']->getDecoratedService());
$this->assertEquals(['decorated', 'decorated.pif-pouf', 5], $services['decorator_service_with_name_and_priority']->getDecoratedService());
$this->assertEquals(['decorated', 'decorated.pif-pouf', 5, ContainerInterface::IGNORE_ON_INVALID_REFERENCE], $services['decorator_service_with_name_and_priority_and_on_invalid']->getDecoratedService());
}
public function testDeprecatedAliases()
@ -546,6 +548,14 @@ class YamlFileLoaderTest extends TestCase
$loader->load('bad_decorates.yml');
}
public function testDecoratedServicesWithWrongOnInvalidSyntaxThrowsException()
{
$this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException');
$this->expectExceptionMessage('Did you mean null (without quotes)');
$loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('bad_decoration_on_invalid_null.yml');
}
public function testInvalidTagsWithDefaults()
{
$this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException');

View File

@ -40,12 +40,18 @@ class JsonErrorRenderer implements ErrorRendererInterface
{
$debug = $this->debug && ($exception->getHeaders()['X-Debug'] ?? true);
if ($debug) {
$message = $exception->getMessage();
} else {
$message = 404 === $exception->getStatusCode() ? 'Sorry, the page you are looking for could not be found.' : 'Whoops, looks like something went wrong.';
}
$content = [
'title' => $exception->getTitle(),
'status' => $exception->getStatusCode(),
'detail' => $message,
];
if ($debug) {
$content['detail'] = $exception->getMessage();
$content['exceptions'] = $exception->toArray();
}

View File

@ -39,12 +39,18 @@ class TxtErrorRenderer implements ErrorRendererInterface
public function render(FlattenException $exception): string
{
$debug = $this->debug && ($exception->getHeaders()['X-Debug'] ?? true);
$content = sprintf("[title] %s\n", $exception->getTitle());
$content .= sprintf("[status] %s\n", $exception->getStatusCode());
if ($debug) {
$content .= sprintf("[detail] %s\n", $exception->getMessage());
$message = $exception->getMessage();
} else {
$message = 404 === $exception->getStatusCode() ? 'Sorry, the page you are looking for could not be found.' : 'Whoops, looks like something went wrong.';
}
$content = sprintf("[title] %s\n", $exception->getTitle());
$content .= sprintf("[status] %s\n", $exception->getStatusCode());
$content .= sprintf("[detail] %s\n", $message);
if ($debug) {
foreach ($exception->toArray() as $i => $e) {
$content .= sprintf("[%d] %s: %s\n", $i + 1, $e['class'], $e['message']);
foreach ($e['trace'] as $trace) {

View File

@ -42,14 +42,16 @@ class XmlErrorRenderer implements ErrorRendererInterface
{
$debug = $this->debug && ($exception->getHeaders()['X-Debug'] ?? true);
$title = $this->escapeXml($exception->getTitle());
if ($debug) {
$message = $this->escapeXml($exception->getMessage());
} else {
$message = 404 === $exception->getStatusCode() ? 'Sorry, the page you are looking for could not be found.' : 'Whoops, looks like something went wrong.';
}
$statusCode = $this->escapeXml($exception->getStatusCode());
$charset = $this->escapeXml($this->charset);
$exceptions = '';
$message = '';
if ($debug) {
$message = '<detail>'.$this->escapeXml($exception->getMessage()).'</detail>';
$exceptions .= '<exceptions>';
foreach ($exception->toArray() as $e) {
$exceptions .= sprintf('<exception class="%s" message="%s"><traces>', $e['class'], $this->escapeXml($e['message']));
@ -73,7 +75,7 @@ class XmlErrorRenderer implements ErrorRendererInterface
<problem xmlns="urn:ietf:rfc:7807">
<title>{$title}</title>
<status>{$statusCode}</status>
{$message}
<detail>{$message}</detail>
{$exceptions}
</problem>
EOF;

View File

@ -56,7 +56,8 @@ TXT
$this->assertSame(<<<TXT
{
"title": "Internal Server Error",
"status": 500
"status": 500,
"detail": "Whoops, looks like something went wrong."
}
TXT

View File

@ -44,7 +44,8 @@ JSON;
$expectedNonDebug = <<<JSON
{
"title": "Internal Server Error",
"status": 500
"status": 500,
"detail": "Whoops, looks like something went wrong."
}
JSON;

View File

@ -39,6 +39,7 @@ TXT;
$expectedNonDebug = <<<TXT
[title] Internal Server Error
[status] 500
[detail] Whoops, looks like something went wrong.
TXT;
yield '->render() returns the TXT content WITH stack traces in debug mode' => [

View File

@ -43,7 +43,7 @@ XML;
<problem xmlns="urn:ietf:rfc:7807">
<title>Internal Server Error</title>
<status>500</status>
<detail>Whoops, looks like something went wrong.</detail>
</problem>
XML;

View File

@ -25,6 +25,7 @@ CHANGELOG
4.4.0
-----
* add new `WeekType`
* using different values for the "model_timezone" and "view_timezone" options of the `TimeType` without configuring a
reference date is deprecated
* preferred choices are repeated in the list of all choices

View File

@ -76,6 +76,7 @@ class CoreExtension extends AbstractExtension
new Type\CurrencyType(),
new Type\TelType(),
new Type\ColorType(),
new Type\WeekType(),
];
}

View File

@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Transforms between an ISO 8601 week date string and an array.
*
* @author Damien Fayet <damienf1521@gmail.com>
*/
class WeekToArrayTransformer implements DataTransformerInterface
{
/**
* Transforms a string containing an ISO 8601 week date into an array.
*
* @param string|null $value A week date string
*
* @return array A value containing year and week
*
* @throws TransformationFailedException If the given value is not a string,
* or if the given value does not follow the right format
*/
public function transform($value)
{
if (null === $value) {
return ['year' => null, 'week' => null];
}
if (!\is_string($value)) {
throw new TransformationFailedException(sprintf('Value is expected to be a string but was "%s".', \is_object($value) ? \get_class($value) : \gettype($value)));
}
if (0 === preg_match('/^(?P<year>\d{4})-W(?P<week>\d{2})$/', $value, $matches)) {
throw new TransformationFailedException('Given data does not follow the date format "Y-\WW".');
}
return [
'year' => (int) $matches['year'],
'week' => (int) $matches['week'],
];
}
/**
* Transforms an array into a week date string.
*
* @param array $value An array containing a year and a week number
*
* @return string|null A week date string following the format Y-\WW
*
* @throws TransformationFailedException If the given value can not be merged in a valid week date string,
* or if the obtained week date does not exists
*/
public function reverseTransform($value)
{
if (null === $value || [] === $value) {
return null;
}
if (!\is_array($value)) {
throw new TransformationFailedException(sprintf('Value is expected to be an array, but was "%s".', \is_object($value) ? \get_class($value) : \gettype($value)));
}
if (!\array_key_exists('year', $value)) {
throw new TransformationFailedException('Key "year" is missing.');
}
if (!\array_key_exists('week', $value)) {
throw new TransformationFailedException('Key "week" is missing.');
}
if ($additionalKeys = array_diff(array_keys($value), ['year', 'week'])) {
throw new TransformationFailedException(sprintf('Expected only keys "year" and "week" to be present, but also got ["%s"].', implode('", "', $additionalKeys)));
}
if (null === $value['year'] && null === $value['week']) {
return null;
}
if (!\is_int($value['year'])) {
throw new TransformationFailedException(sprintf('Year is expected to be an integer, but was "%s".', \is_object($value['year']) ? \get_class($value['year']) : \gettype($value['year'])));
}
if (!\is_int($value['week'])) {
throw new TransformationFailedException(sprintf('Week is expected to be an integer, but was "%s".', \is_object($value['week']) ? \get_class($value['week']) : \gettype($value['week'])));
}
// The 28th December is always in the last week of the year
if (date('W', strtotime('28th December '.$value['year'])) < $value['week']) {
throw new TransformationFailedException(sprintf('Week "%d" does not exist for year "%d".', $value['week'], $value['year']));
}
return sprintf('%d-W%02d', $value['year'], $value['week']);
}
}

View File

@ -0,0 +1,192 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\WeekToArrayTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class WeekType extends AbstractType
{
private static $widgets = [
'text' => IntegerType::class,
'choice' => ChoiceType::class,
];
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ('string' === $options['input']) {
$builder->addModelTransformer(new WeekToArrayTransformer());
}
if ('single_text' === $options['widget']) {
$builder->addViewTransformer(new ReversedTransformer(new WeekToArrayTransformer()));
} else {
$yearOptions = $weekOptions = [
'error_bubbling' => true,
'empty_data' => '',
];
// when the form is compound the entries of the array are ignored in favor of children data
// so we need to handle the cascade setting here
$emptyData = $builder->getEmptyData() ?: [];
$yearOptions['empty_data'] = $emptyData['year'] ?? '';
$weekOptions['empty_data'] = $emptyData['week'] ?? '';
if (isset($options['invalid_message'])) {
$yearOptions['invalid_message'] = $options['invalid_message'];
$weekOptions['invalid_message'] = $options['invalid_message'];
}
if (isset($options['invalid_message_parameters'])) {
$yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
$weekOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
}
if ('choice' === $options['widget']) {
// Only pass a subset of the options to children
$yearOptions['choices'] = array_combine($options['years'], $options['years']);
$yearOptions['placeholder'] = $options['placeholder']['year'];
$yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year'];
$weekOptions['choices'] = array_combine($options['weeks'], $options['weeks']);
$weekOptions['placeholder'] = $options['placeholder']['week'];
$weekOptions['choice_translation_domain'] = $options['choice_translation_domain']['week'];
// Append generic carry-along options
foreach (['required', 'translation_domain'] as $passOpt) {
$yearOptions[$passOpt] = $options[$passOpt];
$weekOptions[$passOpt] = $options[$passOpt];
}
}
$builder->add('year', self::$widgets[$options['widget']], $yearOptions);
$builder->add('week', self::$widgets[$options['widget']], $weekOptions);
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['widget'] = $options['widget'];
if ($options['html5']) {
$view->vars['type'] = 'week';
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$compound = function (Options $options) {
return 'single_text' !== $options['widget'];
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) {
if (\is_array($placeholder)) {
$default = $placeholderDefault($options);
return array_merge(
['year' => $default, 'week' => $default],
$placeholder
);
}
return [
'year' => $placeholder,
'week' => $placeholder,
];
};
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
if (\is_array($choiceTranslationDomain)) {
$default = false;
return array_replace(
['year' => $default, 'week' => $default],
$choiceTranslationDomain
);
}
return [
'year' => $choiceTranslationDomain,
'week' => $choiceTranslationDomain,
];
};
$resolver->setDefaults([
'years' => range(date('Y') - 10, date('Y') + 10),
'weeks' => array_combine(range(1, 53), range(1, 53)),
'widget' => 'single_text',
'input' => 'array',
'placeholder' => $placeholderDefault,
'html5' => static function (Options $options) {
return 'single_text' === $options['widget'];
},
'error_bubbling' => false,
'empty_data' => function (Options $options) {
return $options['compound'] ? [] : '';
},
'compound' => $compound,
'choice_translation_domain' => false,
]);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setNormalizer('html5', function (Options $options, $html5) {
if ($html5 && 'single_text' !== $options['widget']) {
throw new LogicException(sprintf('The "widget" option of %s must be set to "single_text" when the "html5" option is enabled.', self::class));
}
return $html5;
});
$resolver->setAllowedValues('input', [
'string',
'array',
]);
$resolver->setAllowedValues('widget', [
'single_text',
'text',
'choice',
]);
$resolver->setAllowedTypes('years', 'int[]');
$resolver->setAllowedTypes('weeks', 'int[]');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'week';
}
}

View File

@ -2742,4 +2742,87 @@ abstract class AbstractLayoutTest extends FormIntegrationTestCase
[true],
];
}
public function testWeekSingleText()
{
$form = $this->factory->createNamed('holidays', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '1970-W01', [
'input' => 'string',
'widget' => 'single_text',
]);
$this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']],
'/input
[@type="week"]
[@name="holidays"]
[@class="my&class"]
[@value="1970-W01"]
'
);
}
public function testWeekSingleTextNoHtml5()
{
$form = $this->factory->createNamed('holidays', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '1970-W01', [
'input' => 'string',
'widget' => 'single_text',
'html5' => false,
]);
$this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']],
'/input
[@type="text"]
[@name="holidays"]
[@class="my&class"]
[@value="1970-W01"]
'
);
}
public function testWeekChoices()
{
$data = ['year' => date('Y'), 'week' => 1];
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\WeekType', $data, [
'input' => 'array',
'required' => false,
]);
$this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']],
'/div
[@class="my&class"]
[
./select
[@id="name_year"]
[./option[@value="'.$data['year'].'"][@selected="selected"]]
/following-sibling::select
[@id="name_week"]
[./option[@value="'.$data['week'].'"][@selected="selected"]]
]
[count(.//select)=2]'
);
}
public function testWeekText()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '2000-W01', [
'input' => 'string',
'widget' => 'text',
]);
$this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']],
'/div
[@class="my&class"]
[
./input
[@id="name_year"]
[@type="number"]
[@value="2000"]
/following-sibling::input
[@id="name_week"]
[@type="number"]
[@value="1"]
]
[count(./input)=2]'
);
}
}

View File

@ -0,0 +1,119 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\DataTransformer\WeekToArrayTransformer;
class WeekToArrayTransformerTest extends TestCase
{
public function testTransform()
{
$transformer = new WeekToArrayTransformer();
$this->assertSame(['year' => 2019, 'week' => 1], $transformer->transform('2019-W01'));
}
public function testTransformEmpty()
{
$transformer = new WeekToArrayTransformer();
$this->assertSame(['year' => null, 'week' => null], $transformer->transform(null));
}
/**
* @dataProvider transformationFailuresProvider
*/
public function testTransformationFailures($input, string $message)
{
$this->expectException(TransformationFailedException::class);
$this->expectExceptionMessage($message);
$transformer = new WeekToArrayTransformer();
$transformer->transform($input);
}
public function transformationFailuresProvider(): array
{
return [
'malformed string' => ['lorem', 'Given data does not follow the date format "Y-\WW".'],
'non-string' => [[], 'Value is expected to be a string but was "array".'],
];
}
public function testReverseTransform()
{
$transformer = new WeekToArrayTransformer();
$input = [
'year' => 2019,
'week' => 1,
];
$this->assertEquals('2019-W01', $transformer->reverseTransform($input));
}
public function testReverseTransformCompletelyEmpty()
{
$transformer = new WeekToArrayTransformer();
$input = [
'year' => null,
'week' => null,
];
$this->assertNull($transformer->reverseTransform($input));
}
public function testReverseTransformNull()
{
$transformer = new WeekToArrayTransformer();
$this->assertNull($transformer->reverseTransform(null));
}
public function testReverseTransformEmpty()
{
$transformer = new WeekToArrayTransformer();
$this->assertNull($transformer->reverseTransform([]));
}
/**
* @dataProvider reverseTransformationFailuresProvider
*/
public function testReverseTransformFailures($input, string $message)
{
$this->expectException(TransformationFailedException::class);
$this->expectExceptionMessage($message);
$transformer = new WeekToArrayTransformer();
$transformer->reverseTransform($input);
}
public function reverseTransformationFailuresProvider(): array
{
return [
'missing year' => [['week' => 1], 'Key "year" is missing.'],
'missing week' => [['year' => 2019], 'Key "week" is missing.'],
'integer instead of array' => [0, 'Value is expected to be an array, but was "integer"'],
'string instead of array' => ['12345', 'Value is expected to be an array, but was "string"'],
'week invalid' => [['year' => 2019, 'week' => 66], 'Week "66" does not exist for year "2019".'],
'year null' => [['year' => null, 'week' => 1], 'Year is expected to be an integer, but was "NULL".'],
'week null' => [['year' => 2019, 'week' => null], 'Week is expected to be an integer, but was "NULL".'],
'year non-integer' => [['year' => '2019', 'week' => 1], 'Year is expected to be an integer, but was "string".'],
'week non-integer' => [['year' => 2019, 'week' => '1'], 'Week is expected to be an integer, but was "string".'],
'unexpected key' => [['year' => 2019, 'bar' => 'baz', 'week' => 1, 'foo' => 'foobar'], 'Expected only keys "year" and "week" to be present, but also got ["bar", "foo"].'],
];
}
}

View File

@ -0,0 +1,323 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\FormError;
class WeekTypeTest extends BaseTypeTest
{
const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\WeekType';
public function testSubmitArray()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => 'choice',
'input' => 'array',
]);
$form->submit([
'year' => '2019',
'week' => '1',
]);
$this->assertSame(['year' => 2019, 'week' => 1], $form->getData());
}
public function testSubmitString()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'years' => [2019],
'input' => 'string',
'widget' => 'choice',
]);
$form->submit([
'year' => '2019',
'week' => '1',
]);
$this->assertEquals('2019-W01', $form->getData());
}
public function testSubmitStringSingleText()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'years' => [2019],
'input' => 'string',
'widget' => 'single_text',
]);
$form->submit('2019-W01');
$this->assertEquals('2019-W01', $form->getData());
}
public function testPassDefaultPlaceholderToViewIfNotRequired()
{
$view = $this->factory->create(static::TESTED_TYPE, null, [
'required' => false,
'widget' => 'choice',
])
->createView();
$this->assertSame('', $view['year']->vars['placeholder']);
$this->assertSame('', $view['week']->vars['placeholder']);
}
public function testPassNoPlaceholderToViewIfRequired()
{
$view = $this->factory->create(static::TESTED_TYPE, null, [
'required' => true,
'widget' => 'choice',
])
->createView();
$this->assertNull($view['year']->vars['placeholder']);
$this->assertNull($view['week']->vars['placeholder']);
}
public function testPassPlaceholderAsString()
{
$view = $this->factory->create(static::TESTED_TYPE, null, [
'placeholder' => 'Empty',
'widget' => 'choice',
])
->createView();
$this->assertSame('Empty', $view['year']->vars['placeholder']);
$this->assertSame('Empty', $view['week']->vars['placeholder']);
}
public function testPassPlaceholderAsArray()
{
$view = $this->factory->create(static::TESTED_TYPE, null, [
'placeholder' => [
'year' => 'Empty year',
'week' => 'Empty week',
],
'widget' => 'choice',
])
->createView();
$this->assertSame('Empty year', $view['year']->vars['placeholder']);
$this->assertSame('Empty week', $view['week']->vars['placeholder']);
}
public function testPassPlaceholderAsPartialArrayAddEmptyIfNotRequired()
{
$view = $this->factory->create(static::TESTED_TYPE, null, [
'required' => false,
'placeholder' => [
'year' => 'Empty year',
],
'widget' => 'choice',
])
->createView();
$this->assertSame('Empty year', $view['year']->vars['placeholder']);
$this->assertSame('', $view['week']->vars['placeholder']);
}
public function testPassPlaceholderAsPartialArrayAddNullIfRequired()
{
$view = $this->factory->create(static::TESTED_TYPE, null, [
'required' => true,
'placeholder' => [
'year' => 'Empty year',
],
'widget' => 'choice',
])
->createView();
$this->assertSame('Empty year', $view['year']->vars['placeholder']);
$this->assertNull($view['week']->vars['placeholder']);
}
public function testPassHtml5TypeIfSingleTextAndHtml5Format()
{
$view = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => 'single_text',
])
->createView();
$this->assertSame('week', $view->vars['type']);
}
public function testDontPassHtml5TypeIfHtml5NotAllowed()
{
$view = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => 'single_text',
'html5' => false,
])
->createView();
$this->assertArrayNotHasKey('type', $view->vars);
}
public function testDontPassHtml5TypeIfNotSingleText()
{
$view = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => 'text',
])
->createView();
$this->assertArrayNotHasKey('type', $view->vars);
}
public function testYearTypeChoiceErrorsBubbleUp()
{
$error = new FormError('Invalid!');
$form = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => 'choice',
]);
$form['year']->addError($error);
$this->assertSame([], iterator_to_array($form['year']->getErrors()));
$this->assertSame([$error], iterator_to_array($form->getErrors()));
}
public function testWeekTypeChoiceErrorsBubbleUp()
{
$error = new FormError('Invalid!');
$form = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => 'choice',
]);
$form['week']->addError($error);
$this->assertSame([], iterator_to_array($form['week']->getErrors()));
$this->assertSame([$error], iterator_to_array($form->getErrors()));
}
public function testPassDefaultChoiceTranslationDomain()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => 'choice',
]);
$view = $form->createView();
$this->assertFalse($view['year']->vars['choice_translation_domain']);
$this->assertFalse($view['week']->vars['choice_translation_domain']);
}
public function testPassChoiceTranslationDomainAsString()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'choice_translation_domain' => 'messages',
'widget' => 'choice',
]);
$view = $form->createView();
$this->assertSame('messages', $view['year']->vars['choice_translation_domain']);
$this->assertSame('messages', $view['week']->vars['choice_translation_domain']);
}
public function testPassChoiceTranslationDomainAsArray()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'choice_translation_domain' => [
'year' => 'foo',
'week' => 'test',
],
'widget' => 'choice',
]);
$view = $form->createView();
$this->assertSame('foo', $view['year']->vars['choice_translation_domain']);
$this->assertSame('test', $view['week']->vars['choice_translation_domain']);
}
public function testSubmitNull($expected = null, $norm = null, $view = null)
{
$form = $this->factory->create($this->getTestedType(), null, [
'widget' => 'choice',
]);
$form->submit(null);
$this->assertSame(['year' => null, 'week' => null], $form->getData());
$this->assertSame(['year' => null, 'week' => null], $form->getNormData());
$this->assertSame(['year' => null, 'week' => null], $form->getViewData());
}
public function testSubmitFromChoiceEmpty()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => 'choice',
'required' => false,
]);
$form->submit([
'year' => '',
'week' => '',
]);
$this->assertSame(['year' => null, 'week' => null], $form->getData());
}
public function testSubmitNullWithText()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => 'text',
]);
$form->submit(null);
$this->assertSame(['year' => null, 'week' => null], $form->getViewData());
}
public function testSubmitNullWithSingleText()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => 'single_text',
'input' => 'string',
]);
$form->submit(null);
$this->assertNull($form->getData());
$this->assertNull($form->getNormData());
$this->assertSame('', $form->getViewData());
}
public function testSubmitNullUsesDefaultEmptyData($emptyData = [], $expectedData = null)
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'empty_data' => $emptyData,
'widget' => 'choice',
]);
$form->submit(null);
$this->assertSame(['year' => null, 'week' => null], $form->getData());
}
/**
* @dataProvider provideEmptyData
*/
public function testSubmitNullUsesDateEmptyDataString($widget, $emptyData, $expectedData)
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'widget' => $widget,
'empty_data' => $emptyData,
]);
$form->submit(null);
$this->assertSame($expectedData, $form->getData());
}
public function provideEmptyData()
{
return [
'Compound text field' => ['text', ['year' => '2019', 'week' => '1'], ['year' => 2019, 'week' => 1]],
'Compound choice field' => ['choice', ['year' => '2019', 'week' => '1'], ['year' => 2019, 'week' => 1]],
];
}
}

View File

@ -26,11 +26,19 @@ class ErrorChunk implements ChunkInterface
private $errorMessage;
private $error;
public function __construct(int $offset, \Throwable $error = null)
/**
* @param \Throwable|string $error
*/
public function __construct(int $offset, $error)
{
$this->offset = $offset;
$this->error = $error;
$this->errorMessage = null !== $error ? $error->getMessage() : 'Reading from the response stream reached the idle timeout.';
if (\is_string($error)) {
$this->errorMessage = $error;
} else {
$this->error = $error;
$this->errorMessage = $error->getMessage();
}
}
/**

View File

@ -49,6 +49,8 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
*/
private $multi;
private static $curlVersion;
/**
* @param array $defaultOptions Default requests' options
* @param int $maxHostConnections The maximum number of connections to a single host
@ -69,6 +71,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
}
$this->multi = $multi = new CurlClientState();
self::$curlVersion = self::$curlVersion ?? curl_version();
// Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
if (\defined('CURLPIPE_MULTIPLEX')) {
@ -87,7 +90,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
}
// HTTP/2 push crashes before curl 7.61
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073d00 > ($v = curl_version())['version_number'] || !(CURL_VERSION_HTTP2 & $v['features'])) {
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073d00 > self::$curlVersion['version_number'] || !(CURL_VERSION_HTTP2 & self::$curlVersion['features'])) {
return;
}
@ -192,7 +195,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
$this->multi->dnsCache->evictions = [];
$port = parse_url($authority, PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
if ($resolve && 0x072a00 > curl_version()['version_number']) {
if ($resolve && 0x072a00 > self::$curlVersion['version_number']) {
// DNS cache removals require curl 7.42 or higher
// On lower versions, we have to create a new multi handle
curl_multi_close($this->multi->handle);
@ -212,7 +215,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
$curlopts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
} elseif (1.1 === (float) $options['http_version'] || 'https:' !== $scheme) {
$curlopts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
} elseif (\defined('CURL_VERSION_HTTP2') && CURL_VERSION_HTTP2 & curl_version()['features']) {
} elseif (\defined('CURL_VERSION_HTTP2') && CURL_VERSION_HTTP2 & self::$curlVersion['features']) {
$curlopts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
}

View File

@ -25,7 +25,9 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
*/
final class CurlResponse implements ResponseInterface
{
use ResponseTrait;
use ResponseTrait {
getContent as private doGetContent;
}
private static $performing = false;
private $multi;
@ -60,7 +62,7 @@ final class CurlResponse implements ResponseInterface
if (!$info['response_headers']) {
// Used to keep track of what we're waiting for
curl_setopt($ch, CURLOPT_PRIVATE, 'headers');
curl_setopt($ch, CURLOPT_PRIVATE, \in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true) && 1.0 < (float) ($options['http_version'] ?? 1.1) ? 'H2' : 'H0'); // H = headers + retry counter
}
if (null === $content = &$this->content) {
@ -139,7 +141,7 @@ final class CurlResponse implements ResponseInterface
$waitFor = curl_getinfo($ch = $response->handle, CURLINFO_PRIVATE);
if (\in_array($waitFor, ['headers', 'destruct'], true)) {
if ('H' === $waitFor[0] || 'D' === $waitFor[0]) {
try {
foreach (self::stream([$response]) as $chunk) {
if ($chunk->isFirst()) {
@ -153,16 +155,11 @@ final class CurlResponse implements ResponseInterface
throw $e;
}
}
curl_setopt($ch, CURLOPT_HEADERFUNCTION, null);
curl_setopt($ch, CURLOPT_READFUNCTION, null);
curl_setopt($ch, CURLOPT_INFILE, null);
};
// Schedule the request in a non-blocking way
$multi->openHandles[$id] = [$ch, $options];
curl_multi_add_handle($multi->handle, $ch);
self::perform($multi);
}
/**
@ -171,8 +168,6 @@ final class CurlResponse implements ResponseInterface
public function getInfo(string $type = null)
{
if (!$info = $this->finalInfo) {
self::perform($this->multi);
$info = array_merge($this->info, curl_getinfo($this->handle));
$info['url'] = $this->info['url'] ?? $info['url'];
$info['redirect_url'] = $this->info['redirect_url'] ?? null;
@ -185,8 +180,9 @@ final class CurlResponse implements ResponseInterface
rewind($this->debugBuffer);
$info['debug'] = stream_get_contents($this->debugBuffer);
$waitFor = curl_getinfo($this->handle, CURLINFO_PRIVATE);
if (!\in_array(curl_getinfo($this->handle, CURLINFO_PRIVATE), ['headers', 'content'], true)) {
if ('H' !== $waitFor[0] && 'C' !== $waitFor[0]) {
curl_setopt($this->handle, CURLOPT_VERBOSE, false);
rewind($this->debugBuffer);
ftruncate($this->debugBuffer, 0);
@ -197,6 +193,21 @@ final class CurlResponse implements ResponseInterface
return null !== $type ? $info[$type] ?? null : $info;
}
/**
* {@inheritdoc}
*/
public function getContent(bool $throw = true): string
{
$performing = self::$performing;
self::$performing = $performing || '_0' === curl_getinfo($this->handle, CURLINFO_PRIVATE);
try {
return $this->doGetContent($throw);
} finally {
self::$performing = $performing;
}
}
public function __destruct()
{
try {
@ -204,10 +215,13 @@ final class CurlResponse implements ResponseInterface
return; // Unused pushed response
}
if ('content' === $waitFor = curl_getinfo($this->handle, CURLINFO_PRIVATE)) {
$waitFor = curl_getinfo($this->handle, CURLINFO_PRIVATE);
if ('C' === $waitFor[0] || '_' === $waitFor[0]) {
$this->close();
} elseif ('headers' === $waitFor) {
curl_setopt($this->handle, CURLOPT_PRIVATE, 'destruct');
} elseif ('H' === $waitFor[0]) {
$waitFor[0] = 'D'; // D = destruct
curl_setopt($this->handle, CURLOPT_PRIVATE, $waitFor);
}
$this->doDestruct();
@ -245,7 +259,7 @@ final class CurlResponse implements ResponseInterface
unset($this->multi->openHandles[$this->id], $this->multi->handlesActivity[$this->id]);
curl_multi_remove_handle($this->multi->handle, $this->handle);
curl_setopt_array($this->handle, [
CURLOPT_PRIVATE => '',
CURLOPT_PRIVATE => '_0',
CURLOPT_NOPROGRESS => true,
CURLOPT_PROGRESSFUNCTION => null,
CURLOPT_HEADERFUNCTION => null,
@ -266,7 +280,7 @@ final class CurlResponse implements ResponseInterface
$runningResponses[$i] = [$response->multi, [$response->id => $response]];
}
if ('' === curl_getinfo($ch = $response->handle, CURLINFO_PRIVATE)) {
if ('_0' === curl_getinfo($ch = $response->handle, CURLINFO_PRIVATE)) {
// Response already completed
$response->multi->handlesActivity[$response->id][] = null;
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
@ -294,8 +308,26 @@ final class CurlResponse implements ResponseInterface
while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($multi->handle, $active));
while ($info = curl_multi_info_read($multi->handle)) {
$multi->handlesActivity[(int) $info['handle']][] = null;
$multi->handlesActivity[(int) $info['handle']][] = \in_array($info['result'], [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || (\CURLE_WRITE_ERROR === $info['result'] && 'destruct' === @curl_getinfo($info['handle'], CURLINFO_PRIVATE)) ? null : new TransportException(sprintf('%s for "%s".', curl_strerror($info['result']), curl_getinfo($info['handle'], CURLINFO_EFFECTIVE_URL)));
$result = $info['result'];
$id = (int) $ch = $info['handle'];
$waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE) ?: '_0';
if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /*CURLE_HTTP2*/ 16, /*CURLE_HTTP2_STREAM*/ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
curl_multi_remove_handle($multi->handle, $ch);
$waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
curl_setopt($ch, CURLOPT_PRIVATE, $waitFor);
if ('1' === $waitFor[1]) {
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
}
if (0 === curl_multi_add_handle($multi->handle, $ch)) {
continue;
}
}
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(sprintf('%s for "%s".', curl_strerror($result), curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)));
}
} finally {
self::$performing = false;
@ -320,7 +352,9 @@ final class CurlResponse implements ResponseInterface
*/
private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger, &$content = null): int
{
if (!\in_array($waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE), ['headers', 'destruct'], true)) {
$waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE) ?: '_0';
if ('H' !== $waitFor[0] && 'D' !== $waitFor[0]) {
return \strlen($data); // Ignore HTTP trailers
}
@ -381,14 +415,18 @@ final class CurlResponse implements ResponseInterface
}
if ($statusCode < 300 || 400 <= $statusCode || null === $location || curl_getinfo($ch, CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
// Headers and redirects completed, time to get the response's body
// Headers and redirects completed, time to get the response's content
$multi->handlesActivity[$id][] = new FirstChunk();
if ('destruct' === $waitFor) {
return 0;
if ('D' === $waitFor[0] || 'HEAD' === $info['http_method'] || \in_array($statusCode, [204, 304], true)) {
$waitFor = '_0'; // no content expected
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = null;
} else {
$waitFor[0] = 'C'; // C = content
}
curl_setopt($ch, CURLOPT_PRIVATE, 'content');
curl_setopt($ch, CURLOPT_PRIVATE, $waitFor);
try {
if (!$content && $options['buffer'] instanceof \Closure && $content = $options['buffer']($headers) ?: null) {

View File

@ -291,7 +291,7 @@ class MockResponse implements ResponseInterface
foreach ($body as $chunk) {
if ('' === $chunk = (string) $chunk) {
// simulate an idle timeout
$response->body[] = new ErrorChunk($offset);
$response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url']));
} else {
$response->body[] = $chunk;
$offset += \strlen($chunk);

Some files were not shown because too many files have changed in this diff Show More