diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 30408a440624e..6a3604dff7aad 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 6.3 for features / 4.4, 5.4, 6.0, 6.1, or 6.2 for bug fixes +| Branch? | 6.3 for features / 5.4, 6.0, 6.1, or 6.2 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no @@ -16,7 +16,7 @@ Additionally (see https://symfony.com/releases): - Bug fixes must be submitted against the lowest maintained branch where they apply (lowest branches are regularly merged to upper ones so they get the fixes too). - Features and deprecations must be submitted against the latest branch. - - For new features, provide some code snippets to help understand usage. + - For new features, provide some code snippets to help understand usage. - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry - Never break backward compatibility (see https://symfony.com/bc). --> diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index f806bdc816901..931fba325e7f0 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -48,7 +48,7 @@ jobs: ports: - 16379:6379 redis-cluster: - image: grokzen/redis-cluster:5.0.4 + image: grokzen/redis-cluster:latest ports: - 7000:7000 - 7001:7001 diff --git a/CHANGELOG-6.0.md b/CHANGELOG-6.0.md index ee6636841aa4a..d0024faa6a47d 100644 --- a/CHANGELOG-6.0.md +++ b/CHANGELOG-6.0.md @@ -7,6 +7,35 @@ in 6.0 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v6.0.0...v6.0.1 +* 6.0.16 (2022-11-28) + + * bug #48333 [Yaml] parse unquoted digits in tag values as integers (xabbuh) + * bug #48330 [FrameworkBundle] do not wire the MercureTransportFactory if the MercureBundle is not enabled (xabbuh) + * bug #48262 [Notifier] [SMSBiuras] `true`/`false` mismatch for `test_mode` option (StaffNowa) + * bug #48273 [HttpKernel] Fix message for unresovable arguments of invokable controllers (fancyweb) + * bug #48251 [PropertyInfo] ignore const expressions read by phpdocumentor (xabbuh) + * bug #48224 [DependencyInjection] Process bindings in `ServiceLocatorTagPass` (MatTheCat) + * bug #48179 [Console] Support completion for bash functions (Chi-teck) + * bug #48217 [Console] Improve error message when shell is not detected in completion command (GromNaN) + * bug #48222 [Translation] [Lokalize] Configure `replace_breaks` to prevent issues with multilines translations (Kocal) + * bug #48210 [Console]  Fix signal handlers called after event listeners and skip exit (GromNaN) + * bug #48198 [Messenger] Fix time-limit check exception (alamirault) + * bug #48122 [PhpUnitBridge] Fix language deprecations incorrectly marked as direct (wouterj) + * bug #47998 [Console] Fix console `ProgressBar::override()` after manual `ProgressBar::cleanup()` (maxbeckers) + * bug #48173 [HttpClient] Handle Amp HTTP client v5 incompatibility gracefully (fancyweb) + * bug #48172 [HttpKernel] Don’t try to wire Response argument with controller.service_arguments (MatTheCat) + * bug #48085 [Messenger] Tell about messenger:consume invalid limit options (MatTheCat) + * bug #48120 [Messenger] Do not throw 'no handlers' exception when skipping handlers due to duplicate handling (wouterj) + * bug #48112 [HttpFoundation] Compare cookie with null value as empty string in ResponseCookieValueSame (fancyweb) + * bug #48119 [FrameworkBundle][Lock] Allow to disable lock without defining a resource (MatTheCat) + * bug #48093 [DependencyInjection] don't move locator tag for service subscriber (RobertMe) + * bug #48075 [Mailer] Stream timeout not detected fgets returns false (Sezil) + * bug #48092 Fix the notification email theme for asynchronously dispatched emails (krisbuist) + * bug #48097 Fix search scope when performing fallback mapping driver detection (spideyfusion) + * bug #48103 [HttpClient] Do not set http_version instead of setting it to null (Tetragramat) + * bug #48027 [DependencyInjection] Don't autoconfigure tag when it's already set with attributes (nicolas-grekas) + * bug #48050 [HttpFoundation] Check IPv6 is valid before comparing it (PhilETaylor) + * 6.0.15 (2022-10-28) * bug #47990 [HttpClient] Fix retrying requests when the content is used by the strategy (nicolas-grekas) diff --git a/CHANGELOG-6.1.md b/CHANGELOG-6.1.md index 10ba1bca4466b..ec054deff3146 100644 --- a/CHANGELOG-6.1.md +++ b/CHANGELOG-6.1.md @@ -7,6 +7,47 @@ in 6.1 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v6.1.0...v6.1.1 +* 6.1.9 (2022-12-28) + + * bug #48787 [PhpUnitBridge] Use verbose deprecation output for quiet types only when it reaches the threshold (ogizanagi) + * bug #48784 [Console] Correctly overwrite progressbars with different line count per step (ncharalampidis) + * bug #48801 [Form] Make `ButtonType` handle `form_attr` option (MatTheCat) + * bug #48791 [DependencyInjection] Fix deduplicating service instances in circular graphs (nicolas-grekas) + * bug #48771 [CssSelector] Fix escape patterns (fancyweb) + * bug #48711 [Cache] RedisTrait::createConnection does not pass auth value from redis sentinel cluster DSN (evgkord) + * bug #48724 [VarExporter] Fix exporting classes with __unserialize() but not __serialize() (fancyweb) + * bug #48746 [Validator] Fix IBAN format for Tunisia and Mauritania (smelesh) + * bug #48738 [Workflow] Allow spaces in place names so the PUML dump doesn't break (Kamil Musial) + * bug #48718 Compatibility with doctrine/annotations 2 (derrabus) + * bug #48681 [Console] Revert "bug #48089 Fix clear line with question in section (maxbeckers) (chalasr) + * bug #48651 [HttpKernel] AbstractSessionListener should not override the cache lifetime for private responses (rodmen) + * bug #48591 [DependencyInjection] Shared private services becomes public after a public service is accessed (alexpott) + * bug #48126 [Mailer] Include all transports' debug messages in RoundRobin transport exception (mixdf) + * bug #48089 [Console] Fix clear line with question in section (maxbeckers) + * bug #48602 [HtmlSanitizer] Fix HtmlSanitizer default configuration behavior for allowed schemes (Titouan Galopin) + * bug #48635 [HttpFoundation] Use relative timestamps with MemcachedSessionHandler (tvlooy) + * bug #47979 [Cache] Fix dealing with ext-redis' multi/exec returning a bool (João Nogueira) + * bug #48612 [Messenger] [Amqp] Added missing rpc_timeout option (lyrixx) + * bug #48233 [Serializer] Prevent `GetSetMethodNormalizer` from creating invalid magic method call (klaussilveira) + * bug #48628 [HttpFoundation] Fix dumping array cookies (nicolas-grekas) + * bug #48048 [WebProfilerBundle] Fix dump header not being displayed (HypeMC) + * bug #47836 [HttpClient] TraceableHttpClient: increase decorator's priority (adpeyre) + * bug #48259 [FrameworkBundle] Allow configuring `framework.exceptions` with a config builder (MatTheCat) + * bug #48314 [Mime] Fix MessagePart serialization (Amunak) + * bug #48331 [Yaml] fix dumping top-level tagged values (xabbuh) + * bug #48615 Fix getting the name of closures on PHP 8.1.11+ (nicolas-grekas) + * bug #48624 [ErrorHandler][HttpKernel] Fix reading the SYMFONY_IDE env var (nicolas-grekas) + * bug #48618 [ErrorHandler] [DebugClassLoader] Fix some new return types support (fancyweb) + * bug #48421 [HttpFoundation] IPv4-mapped IPv6 addresses incorrectly rejected (bonroyage) + * bug #48501 [RateLimiter] Add `int` to `Reservation::wait()` (DaRealFreak) + * bug #48359 [VarDumper] Ignore \Error in __debugInfo() (fancyweb) + * bug #48534 [FrameworkBundle] add `kernel.locale_aware` tag to `LocaleSwitcher` (kbond) + * bug #48482 [DependencyInjection] Revert "bug #48027 Don't autoconfigure tag when it's already set with attributes" (nicolas-grekas) + * bug #48346 [HttpKernel] In DateTimeValueResolver, convert previously defined date attribute to the expected class (GromNaN) + * bug #48335 [TwigBridge] Amend `MoneyType` twig to include a space (mogilvie) + * bug #48046 [WebProfilerBundle] Remove redundant code from logger template (HypeMC) + * bug #48292 [Security] [LoginLink] Throw InvalidLoginLinkException on missing parameter (MatTheCat) + * 6.1.8 (2022-11-28) * bug #48333 [Yaml] parse unquoted digits in tag values as integers (xabbuh) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 72ef68321528f..b136e74da6266 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -89,6 +89,7 @@ The Symfony Connect username in parenthesis allows to get more information - Eriksen Costa - Florin Patan (florinpatan) - Peter Rehm (rpet) + - Mathieu Lechat (mat_the_cat) - Henrik Bjørnskov (henrikbjorn) - David Buchmann (dbu) - Konstantin Myakshin (koc) @@ -105,6 +106,7 @@ The Symfony Connect username in parenthesis allows to get more information - Issei Murasawa (issei_m) - Fran Moreno (franmomu) - Malte Schlüter (maltemaltesich) + - Antoine Lamirault - Vasilij Dusko - Denis (yethee) - Arnout Boks (aboks) @@ -112,11 +114,9 @@ The Symfony Connect username in parenthesis allows to get more information - Przemysław Bogusz (przemyslaw-bogusz) - Henrik Westphal (snc) - Dariusz Górecki (canni) - - Mathieu Lechat (mat_the_cat) - Maxime Helias (maxhelias) - Ener-Getick - Ruud Kamphuis (ruudk) - - Antoine Lamirault - Sebastiaan Stok (sstok) - Jérôme Vasseur (jvasseur) - Ion Bazan (ionbazan) @@ -159,25 +159,26 @@ The Symfony Connect username in parenthesis allows to get more information - Jesse Rushlow (geeshoe) - Théo FIDRY - jeremyFreeAgent (jeremyfreeagent) + - Jeroen Spee (jeroens) - Michael Babker (mbabker) - Włodzimierz Gajda (gajdaw) - Christian Scheb - Guillaume (guill) - Tugdual Saunier (tucksaun) - Jacob Dreesen (jdreesen) - - Jeroen Spee (jeroens) - Joel Wurtz (brouznouf) - Olivier Dolbeau (odolbeau) - Florian Voutzinos (florianv) - zairig imad (zairigimad) - Colin Frei + - Christopher Hertel (chertel) - Javier Spagnoletti (phansys) - excelwebzone + - Phil Taylor (prazgod) - Jérôme Parmentier (lctrs) - HeahDude - Richard van Laak (rvanlaak) - Paráda József (paradajozsef) - - Christopher Hertel (chertel) - Alessandro Lai (jean85) - Alexander Schwenn (xelaris) - Fabien Pennequin (fabienpennequin) @@ -192,6 +193,7 @@ The Symfony Connect username in parenthesis allows to get more information - Baptiste Leduc (korbeil) - Marco Pivetta (ocramius) - Robert Schönthal (digitalkaoz) + - Hugo Alliaume (kocal) - Võ Xuân Tiến (tienvx) - fd6130 (fdtvui) - Tigran Azatyan (tigranazatyan) @@ -209,13 +211,13 @@ The Symfony Connect username in parenthesis allows to get more information - Jhonny Lidfors (jhonne) - Martin Hujer (martinhujer) - Wouter J + - Chi-teck - Guilliam Xavier - Antonio Pauletich (x-coder264) - Timo Bakx (timobakx) - Juti Noppornpitak (shiroyuki) - Joe Bennett (kralos) - Nate Wiebe (natewiebe13) - - Hugo Alliaume (kocal) - Anthony MARTIN - Colin O'Dell (colinodell) - Sebastian Hörl (blogsh) @@ -227,11 +229,11 @@ The Symfony Connect username in parenthesis allows to get more information - Albert Casademont (acasademont) - Arnaud Kleinpeter (nanocom) - Guilherme Blanco (guilhermeblanco) - - Chi-teck - Michael Voříšek - Farhad Safarov (safarov) - SpacePossum - Pablo Godel (pgodel) + - Denis Brumann (dbrumann) - Romaric Drigon (romaricdrigon) - Andréia Bohner (andreia) - Jannik Zschiesche @@ -249,7 +251,6 @@ The Symfony Connect username in parenthesis allows to get more information - Hubert Lenoir (hubert_lenoir) - Florent Mata (fmata) - mcfedr (mcfedr) - - Denis Brumann (dbrumann) - Maciej Malarz (malarzm) - Soner Sayakci - Artem Lopata @@ -288,7 +289,6 @@ The Symfony Connect username in parenthesis allows to get more information - Artur Kotyrba - Tyson Andre - Thomas Landauer (thomas-landauer) - - Phil Taylor (prazgod) - GDIBass - Samuel NELA (snela) - dFayet @@ -543,6 +543,7 @@ The Symfony Connect username in parenthesis allows to get more information - Chris Smith (cs278) - Florian Klein (docteurklein) - Bilge + - Cătălin Dan (dancatalin) - Rhodri Pugh (rodnaph) - Manuel Kiessling (manuelkiessling) - Patrick Reimers (preimers) @@ -570,6 +571,7 @@ The Symfony Connect username in parenthesis allows to get more information - Evert Harmeling (evertharmeling) - Jan Decavele (jandc) - Gustavo Piltcher + - Shakhobiddin - Grenier Kévin (mcsky_biig) - Stepan Tanasiychuk (stfalcon) - Tiago Ribeiro (fixe) @@ -677,6 +679,7 @@ The Symfony Connect username in parenthesis allows to get more information - Tomasz Kusy - Jakub Kucharovic (jkucharovic) - Kristen Gilden + - Oleksiy (alexndlm) - Robbert Klarenbeek (robbertkl) - Eric Masoero (eric-masoero) - Michael Lutz @@ -701,7 +704,6 @@ The Symfony Connect username in parenthesis allows to get more information - ShinDarth - Arun Philip - Stéphane PY (steph_py) - - Cătălin Dan (dancatalin) - Philipp Kräutli (pkraeutli) - Carl Casbolt (carlcasbolt) - battye @@ -770,7 +772,6 @@ The Symfony Connect username in parenthesis allows to get more information - Tomasz Ignatiuk - Joachim Løvgaard (loevgaard) - vladimir.reznichenko - - Shakhobiddin - Kai - Lee Rowlands - Alain Hippolyte (aloneh) @@ -811,6 +812,7 @@ The Symfony Connect username in parenthesis allows to get more information - Rimas Kudelis - Ben Scott (bpscott) - Andrii Dembitskyi + - a.dmitryuk - Pavol Tuka - Paulo Ribeiro (paulo) - Marc Laporte @@ -1014,7 +1016,6 @@ The Symfony Connect username in parenthesis allows to get more information - Elan Ruusamäe (glen) - Brad Jones - Nicolas de Marqué (nicola) - - a.dmitryuk - Jannik Zschiesche - Jan Ole Behrens (deegital) - Mantas Var (mvar) @@ -1188,6 +1189,7 @@ The Symfony Connect username in parenthesis allows to get more information - Timothée BARRAY - Nilmar Sanchez Muguercia - Ivo Bathke (ivoba) + - Lukas Mencl - Strate - Anton A. Sumin - Atthaphon Urairat @@ -1336,7 +1338,6 @@ The Symfony Connect username in parenthesis allows to get more information - Massimiliano Braglia (massimilianobraglia) - Swen van Zanten (swenvanzanten) - Frankie Wittevrongel - - Oleksiy (alexndlm) - Richard Quadling - James Hudson (mrthehud) - Adam Prickett @@ -2069,7 +2070,6 @@ The Symfony Connect username in parenthesis allows to get more information - Pablo Borowicz - Ondřej Frei - Máximo Cuadros (mcuadros) - - Lukas Mencl - EXT - THERAGE Kevin - tamirvs - gauss @@ -2252,6 +2252,7 @@ The Symfony Connect username in parenthesis allows to get more information - Alain Flaus (halundra) - tsufeki - Philipp Strube + - Petar Obradović - Clement Herreman (clemherreman) - Dan Ionut Dumitriu (danionut90) - Evgeny (disparity) @@ -2278,6 +2279,7 @@ The Symfony Connect username in parenthesis allows to get more information - cilefen (cilefen) - Mo Di (modi) - Pablo Schläpfer + - Robert Meijers - Xavier RENAUDIN - Christian Wahler (christian) - Jelte Steijaert (jelte) @@ -2325,6 +2327,7 @@ The Symfony Connect username in parenthesis allows to get more information - Balázs Benyó (duplabe) - Erika Heidi Reinaldo (erikaheidi) - Marc J. Schmidt (marcjs) + - Maximilian Beckers (maxbeckers) - Sebastian Schwarz - karolsojko - Marco Jantke @@ -2631,6 +2634,7 @@ The Symfony Connect username in parenthesis allows to get more information - Bert Hekman - Luis Muñoz - Matthew Donadio + - Kris Buist - Houziaux mike - Phobetor - Markus @@ -2728,6 +2732,7 @@ The Symfony Connect username in parenthesis allows to get more information - Rénald Casagraude (rcasagraude) - Robin Duval (robin-duval) - Mohammad Ali Sarbanha (sarbanha) + - Steeve Titeca (stiteca) - Artem Lopata (bumz) - alex - Roman Orlov @@ -2825,6 +2830,7 @@ The Symfony Connect username in parenthesis allows to get more information - pf - Zoli Konta - Vincent Chalnot + - Roeland Jago Douma - Patrizio Bekerle - Tom Maguire - Mateusz Lerczak @@ -2849,6 +2855,7 @@ The Symfony Connect username in parenthesis allows to get more information - Omar Yepez (oyepez003) - Jonny Schmid (schmidjon) - Götz Gottwald + - Christoph Krapp - Nick Chiu - Robert Campbell - Matt Lehner @@ -3009,6 +3016,7 @@ The Symfony Connect username in parenthesis allows to get more information - Mantas Urnieža - temperatur - Paul Andrieux + - Sezil - Cas - ghazy ben ahmed - Karolis @@ -3076,6 +3084,7 @@ The Symfony Connect username in parenthesis allows to get more information - Markus Tacker - Kasperki - Tammy D + - Adrien Foulon - Ryan Rud - Ondrej Slinták - vlechemin @@ -3122,6 +3131,7 @@ The Symfony Connect username in parenthesis allows to get more information - zorn - Yuriy Potemkin - Emilie Lorenzo + - prudhomme victor - enomotodev - Vincent - Benjamin Long @@ -3166,6 +3176,7 @@ The Symfony Connect username in parenthesis allows to get more information - Lin Lu - arduanov - sualko + - Fabien - Martin Komischke - ADmad - Nicolas Roudaire diff --git a/composer.json b/composer.json index 9ca5b7e49f857..d574f9dae52bb 100644 --- a/composer.json +++ b/composer.json @@ -120,7 +120,7 @@ "async-aws/sqs": "^1.0", "async-aws/sns": "^1.0", "cache/integration-tests": "dev-master", - "doctrine/annotations": "^1.13.1", + "doctrine/annotations": "^1.13.1|^2", "doctrine/collections": "^1.0|^2.0", "doctrine/data-fixtures": "^1.1", "doctrine/dbal": "^2.13.1|^3.0", @@ -153,7 +153,7 @@ "egulias/email-validator": "~3.0.0", "masterminds/html5": "<2.6", "phpdocumentor/reflection-docblock": "<5.2", - "phpdocumentor/type-resolver": "<1.5.1", + "phpdocumentor/type-resolver": "<1.5.1|>=1.7.0", "ocramius/proxy-manager": "<2.1", "phpunit/phpunit": "<5.4.3" }, diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php index 31d885e059de7..3771e772afc49 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -15,6 +15,7 @@ use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\ORMSetup; use Doctrine\ORM\Tools\Setup; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; @@ -33,7 +34,9 @@ class DoctrineExtractorTest extends TestCase { private function createExtractor() { - $config = Setup::createAnnotationMetadataConfiguration([__DIR__.\DIRECTORY_SEPARATOR.'Fixtures'], true); + $config = class_exists(ORMSetup::class) + ? ORMSetup::createAnnotationMetadataConfiguration([__DIR__.\DIRECTORY_SEPARATOR.'Fixtures'], true) + : Setup::createAnnotationMetadataConfiguration([__DIR__.\DIRECTORY_SEPARATOR.'Fixtures'], true); $entityManager = EntityManager::create(['driver' => 'pdo_sqlite'], $config); if (!DBALType::hasType('foo')) { diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 713d5c84c8a66..3ded2fb48581c 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -42,7 +42,7 @@ "symfony/validator": "^5.4|^6.0", "symfony/translation": "^5.4|^6.0", "symfony/var-dumper": "^5.4|^6.0", - "doctrine/annotations": "^1.10.4", + "doctrine/annotations": "^1.10.4|^2", "doctrine/collections": "^1.0|^2.0", "doctrine/data-fixtures": "^1.1", "doctrine/dbal": "^2.13.1|^3.0", diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php index 81613fe21e0e0..a5c877b296681 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php @@ -60,15 +60,15 @@ public function isActivatedProvider(): array { return [ ['/test', RecordFactory::create(Logger::ERROR), true], - ['/400', RecordFactory::create(Logger::ERROR, context: $this->getContextException(400)), true], - ['/400/a', RecordFactory::create(Logger::ERROR, context: $this->getContextException(400)), false], - ['/400/b', RecordFactory::create(Logger::ERROR, context: $this->getContextException(400)), false], - ['/400/c', RecordFactory::create(Logger::ERROR, context: $this->getContextException(400)), true], - ['/401', RecordFactory::create(Logger::ERROR, context: $this->getContextException(401)), true], - ['/403', RecordFactory::create(Logger::ERROR, context: $this->getContextException(403)), false], - ['/404', RecordFactory::create(Logger::ERROR, context: $this->getContextException(404)), false], - ['/405', RecordFactory::create(Logger::ERROR, context: $this->getContextException(405)), false], - ['/500', RecordFactory::create(Logger::ERROR, context: $this->getContextException(500)), true], + ['/400', RecordFactory::create(Logger::ERROR, context: self::getContextException(400)), true], + ['/400/a', RecordFactory::create(Logger::ERROR, context: self::getContextException(400)), false], + ['/400/b', RecordFactory::create(Logger::ERROR, context: self::getContextException(400)), false], + ['/400/c', RecordFactory::create(Logger::ERROR, context: self::getContextException(400)), true], + ['/401', RecordFactory::create(Logger::ERROR, context: self::getContextException(401)), true], + ['/403', RecordFactory::create(Logger::ERROR, context: self::getContextException(403)), false], + ['/404', RecordFactory::create(Logger::ERROR, context: self::getContextException(404)), false], + ['/405', RecordFactory::create(Logger::ERROR, context: self::getContextException(405)), false], + ['/500', RecordFactory::create(Logger::ERROR, context: self::getContextException(500)), true], ]; } diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php index f58e9afa7164e..220cbe5a78bcd 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php @@ -40,18 +40,18 @@ public function isActivatedProvider(): array { return [ ['/test', RecordFactory::create(Logger::DEBUG), false], - ['/foo', RecordFactory::create(Logger::DEBUG, context: $this->getContextException(404)), false], - ['/baz/bar', RecordFactory::create(Logger::ERROR, context: $this->getContextException(404)), false], - ['/foo', RecordFactory::create(Logger::ERROR, context: $this->getContextException(404)), false], - ['/foo', RecordFactory::create(Logger::ERROR, context: $this->getContextException(500)), true], + ['/foo', RecordFactory::create(Logger::DEBUG, context: self::getContextException(404)), false], + ['/baz/bar', RecordFactory::create(Logger::ERROR, context: self::getContextException(404)), false], + ['/foo', RecordFactory::create(Logger::ERROR, context: self::getContextException(404)), false], + ['/foo', RecordFactory::create(Logger::ERROR, context: self::getContextException(500)), true], ['/test', RecordFactory::create(Logger::ERROR), true], - ['/baz', RecordFactory::create(Logger::ERROR, context: $this->getContextException(404)), true], - ['/baz', RecordFactory::create(Logger::ERROR, context: $this->getContextException(500)), true], + ['/baz', RecordFactory::create(Logger::ERROR, context: self::getContextException(404)), true], + ['/baz', RecordFactory::create(Logger::ERROR, context: self::getContextException(500)), true], ]; } - protected function getContextException(int $code): array + protected static function getContextException(int $code): array { return ['exception' => new HttpException($code)]; } diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php index 8de9a956e7282..6e4b67e265d1d 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php @@ -46,8 +46,8 @@ public function testDatetimeRfc3339Format() public function testDebugProcessor() { $processor = new DebugProcessor(); - $processor($this->getRecord()); - $processor($this->getRecord(Logger::ERROR)); + $processor(self::getRecord()); + $processor(self::getRecord(Logger::ERROR)); $this->assertCount(2, $processor->getLogs()); $this->assertSame(1, $processor->countErrors()); @@ -65,8 +65,8 @@ public function testWithRequestStack() { $stack = new RequestStack(); $processor = new DebugProcessor($stack); - $processor($this->getRecord()); - $processor($this->getRecord(Logger::ERROR)); + $processor(self::getRecord()); + $processor(self::getRecord(Logger::ERROR)); $this->assertCount(2, $processor->getLogs()); $this->assertSame(1, $processor->countErrors()); @@ -74,8 +74,8 @@ public function testWithRequestStack() $request = new Request(); $stack->push($request); - $processor($this->getRecord()); - $processor($this->getRecord(Logger::ERROR)); + $processor(self::getRecord()); + $processor(self::getRecord(Logger::ERROR)); $this->assertCount(4, $processor->getLogs()); $this->assertSame(2, $processor->countErrors()); @@ -99,7 +99,7 @@ public function testInheritedClassCallCountErrorsWithoutArgument() $this->assertEquals(0, $debugProcessorChild->countErrors()); } - private function getRecord($level = Logger::WARNING, $message = 'test'): array|LogRecord + private static function getRecord($level = Logger::WARNING, $message = 'test'): array|LogRecord { return RecordFactory::create($level, $message); } diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index 00bb25a585fd2..36c0960868dad 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -203,7 +203,7 @@ public function shutdown() // store failing status $isFailing = !$configuration->tolerates($this->deprecationGroups); - $this->displayDeprecations($groups, $configuration, $isFailing); + $this->displayDeprecations($groups, $configuration); $this->resetDeprecationGroups(); @@ -216,7 +216,7 @@ public function shutdown() } $isFailingAtShutdown = !$configuration->tolerates($this->deprecationGroups); - $this->displayDeprecations($groups, $configuration, $isFailingAtShutdown); + $this->displayDeprecations($groups, $configuration); if ($configuration->isGeneratingBaseline()) { $configuration->writeBaseline(); @@ -292,11 +292,10 @@ private static function colorize($str, $red) /** * @param string[] $groups * @param Configuration $configuration - * @param bool $isFailing * * @throws \InvalidArgumentException */ - private function displayDeprecations($groups, $configuration, $isFailing) + private function displayDeprecations($groups, $configuration) { $cmp = function ($a, $b) { return $b->count() - $a->count(); @@ -323,7 +322,8 @@ private function displayDeprecations($groups, $configuration, $isFailing) fwrite($handle, "\n".self::colorize($deprecationGroupMessage, 'legacy' !== $group && 'indirect' !== $group)."\n"); } - if ('legacy' !== $group && !$configuration->verboseOutput($group) && !$isFailing) { + // Skip the verbose output if the group is quiet and not failing according to its threshold: + if ('legacy' !== $group && !$configuration->verboseOutput($group) && $configuration->toleratesForGroup($group, $this->deprecationGroups)) { continue; } $notices = $this->deprecationGroups[$group]->notices(); diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php index a432c31183669..45d1ed7ce9940 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php @@ -203,6 +203,32 @@ public function isIgnoredDeprecation(Deprecation $deprecation): bool return (bool) $result; } + /** + * @param array $deprecationGroups + * + * @return bool true if the threshold is not reached for the deprecation type nor for the total + */ + public function toleratesForGroup(string $groupName, array $deprecationGroups): bool + { + $grandTotal = 0; + + foreach ($deprecationGroups as $type => $group) { + if ('legacy' !== $type) { + $grandTotal += $group->count(); + } + } + + if ($grandTotal > $this->thresholds['total']) { + return false; + } + + if (\in_array($groupName, ['self', 'direct', 'indirect'], true) && $deprecationGroups[$groupName]->count() > $this->thresholds[$groupName]) { + return false; + } + + return true; + } + /** * @return bool */ diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php index 0fc2f2d623358..a623edbbf15de 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php @@ -138,7 +138,7 @@ public function startTestSuite($suite) if (!class_exists(AnnotationRegistry::class, false) && class_exists(AnnotationRegistry::class)) { if (method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { AnnotationRegistry::registerUniqueLoader('class_exists'); - } else { + } elseif (method_exists(AnnotationRegistry::class, 'registerLoader')) { AnnotationRegistry::registerLoader('class_exists'); } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php index 2f59ae5b91e2e..496a1b5fa8170 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php @@ -234,6 +234,103 @@ public function testOutputIsNotVerboseInWeakMode() $this->assertFalse($configuration->verboseOutput('other')); } + /** + * @dataProvider provideDataForToleratesForGroup + */ + public function testToleratesForIndividualGroups(string $deprecationsHelper, array $deprecationsPerType, array $expected) + { + $configuration = Configuration::fromUrlEncodedString($deprecationsHelper); + + $groups = $this->buildGroups($deprecationsPerType); + + foreach ($expected as $groupName => $tolerates) { + $this->assertSame($tolerates, $configuration->toleratesForGroup($groupName, $groups), sprintf('Deprecation type "%s" is %s', $groupName, $tolerates ? 'tolerated' : 'not tolerated')); + } + } + + public function provideDataForToleratesForGroup() { + + yield 'total threshold not reached' => ['max[total]=1', [ + 'unsilenced' => 0, + 'self' => 0, + 'legacy' => 1, // Legacy group is ignored in total threshold + 'other' => 0, + 'direct' => 1, + 'indirect' => 0, + ], [ + 'unsilenced' => true, + 'self' => true, + 'legacy' => true, + 'other' => true, + 'direct' => true, + 'indirect' => true, + ]]; + + yield 'total threshold reached' => ['max[total]=1', [ + 'unsilenced' => 0, + 'self' => 0, + 'legacy' => 1, + 'other' => 0, + 'direct' => 1, + 'indirect' => 1, + ], [ + 'unsilenced' => false, + 'self' => false, + 'legacy' => false, + 'other' => false, + 'direct' => false, + 'indirect' => false, + ]]; + + yield 'direct threshold reached' => ['max[total]=99&max[direct]=0', [ + 'unsilenced' => 0, + 'self' => 0, + 'legacy' => 1, + 'other' => 0, + 'direct' => 1, + 'indirect' => 1, + ], [ + 'unsilenced' => true, + 'self' => true, + 'legacy' => true, + 'other' => true, + 'direct' => false, + 'indirect' => true, + ]]; + + yield 'indirect & self threshold reached' => ['max[total]=99&max[direct]=0&max[self]=0', [ + 'unsilenced' => 0, + 'self' => 1, + 'legacy' => 1, + 'other' => 1, + 'direct' => 1, + 'indirect' => 1, + ], [ + 'unsilenced' => true, + 'self' => false, + 'legacy' => true, + 'other' => true, + 'direct' => false, + 'indirect' => true, + ]]; + + yield 'indirect & self threshold not reached' => ['max[total]=99&max[direct]=2&max[self]=2', [ + 'unsilenced' => 0, + 'self' => 1, + 'legacy' => 1, + 'other' => 1, + 'direct' => 1, + 'indirect' => 1, + ], [ + 'unsilenced' => true, + 'self' => true, + 'legacy' => true, + 'other' => true, + 'direct' => true, + 'indirect' => true, + ]]; + } + private function buildGroups($counts) { $groups = []; diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/partially_quiet2.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/partially_quiet2.phpt new file mode 100644 index 0000000000000..4d0d6c3d55794 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/partially_quiet2.phpt @@ -0,0 +1,36 @@ +--TEST-- +Test DeprecationErrorHandler quiet on everything but self/direct deprecations +--FILE-- + +--EXPECTF-- +Unsilenced deprecation notices (3) + +Remaining direct deprecation notices (2) + + 1x: root deprecation + + 1x: silenced bar deprecation + 1x in FooTestCase::testNonLegacyBar + +Remaining indirect deprecation notices (1) + +Legacy deprecation notices (2) diff --git a/src/Symfony/Bridge/PhpUnit/bootstrap.php b/src/Symfony/Bridge/PhpUnit/bootstrap.php index e07c8d6cf5de8..f2064368f41a3 100644 --- a/src/Symfony/Bridge/PhpUnit/bootstrap.php +++ b/src/Symfony/Bridge/PhpUnit/bootstrap.php @@ -30,7 +30,7 @@ if (!class_exists(AnnotationRegistry::class, false) && class_exists(AnnotationRegistry::class)) { if (method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { AnnotationRegistry::registerUniqueLoader('class_exists'); - } else { + } elseif (method_exists(AnnotationRegistry::class, 'registerLoader')) { AnnotationRegistry::registerLoader('class_exists'); } } diff --git a/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php b/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php index e0b3bef29308f..956c5fdd71871 100644 --- a/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php +++ b/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php @@ -35,6 +35,12 @@ public function toName(): string return $this->message->getTo()[0]->getName(); } + /** + * @param string $image A Twig path to the image file. It's recommended to define + * some Twig namespace for email images (e.g. '@email/images/logo.png'). + * @param string|null $contentType The media type (i.e. MIME type) of the image file (e.g. 'image/png'). + * Some email clients require this to display embedded images. + */ public function image(string $image, string $contentType = null): string { $file = $this->twig->getLoader()->getSourceContext($image); @@ -47,6 +53,13 @@ public function image(string $image, string $contentType = null): string return 'cid:'.$image; } + /** + * @param string $file A Twig path to the file. It's recommended to define + * some Twig namespace for email files (e.g. '@email/files/contract.pdf'). + * @param string|null $name A custom file name that overrides the original name of the attached file. + * @param string|null $contentType The media type (i.e. MIME type) of the file (e.g. 'application/pdf'). + * Some email clients require this to display attached files. + */ public function attach(string $file, string $name = null, string $contentType = null): void { $file = $this->twig->getLoader()->getSourceContext($file); diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig index 0e80840541fa1..9aa6081e7e323 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig @@ -6,7 +6,7 @@ {%- set prepend = not (money_pattern starts with '{{') -%} {%- set append = not (money_pattern ends with '}}') -%} {%- if prepend or append -%} -
+
{%- if prepend -%}
{{ money_pattern|form_encode_currency }} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig index 54903a5713082..22555ed88f4a8 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig @@ -6,7 +6,7 @@ {%- set prepend = not (money_pattern starts with '{{') -%} {%- set append = not (money_pattern ends with '}}') -%} {%- if prepend or append -%} -
+
{%- if prepend -%} {{ money_pattern|form_encode_currency }} {%- endif -%} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig index b8cb8c44aa832..e8b9318b3a8bf 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig @@ -11,7 +11,7 @@ {% set prepend = not (money_pattern starts with '{{') %} {% set append = not (money_pattern ends with '}}') %} {% if prepend or append %} -
+
{% if prepend %} {{ money_pattern|form_encode_currency }} {% endif %} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php index 36f8a50d00f13..8689df830b290 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php @@ -1165,7 +1165,7 @@ public function testMoney() $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], '/div - [@class="input-group"] + [@class="input-group "] [ ./div [@class="input-group-prepend"] diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTest.php index 1403372bc5599..d9bb0c17bde97 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTest.php @@ -1450,7 +1450,7 @@ public function testMoney() $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], '/div - [@class="input-group"] + [@class="input-group "] [ ./span [@class="input-group-text"] diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php index b98cb3a73ab4f..00fee10edb2fc 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php @@ -102,7 +102,7 @@ public function testMoneyWidgetInIso() ; $this->assertSame(<<<'HTML' -
+
HTML diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php index 8c0e54744f964..ed69ca81c3375 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php @@ -104,7 +104,7 @@ public function testMoneyWidgetInIso() ->createView(); self::assertSame(<<<'HTML' -
+
HTML , trim($this->renderWidget($view))); } diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index f182dfa406417..7c0bdf7288169 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -21,7 +21,7 @@ "twig/twig": "^2.13|^3.0.4" }, "require-dev": { - "doctrine/annotations": "^1.12", + "doctrine/annotations": "^1.12|^2", "egulias/email-validator": "^2.1.10|^3", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "symfony/asset": "^5.4|^6.0", diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php index 271ba9bf6429b..d4563dbee2d3d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php @@ -67,7 +67,7 @@ protected function configure() For dumping a specific option, add its path as second argument (only available for the yaml format): - php %command.full_name% framework profiler.matcher + php %command.full_name% framework http_client.default_options EOF ) @@ -97,7 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $errorIo->comment([ 'Provide the name of a bundle as the first argument of this command to dump its default configuration. (e.g. config:dump-reference FrameworkBundle)', - 'For dumping a specific option, add its path as the second argument of this command. (e.g. config:dump-reference FrameworkBundle profiler.matcher to dump the framework.profiler.matcher configuration)', + 'For dumping a specific option, add its path as the second argument of this command. (e.g. config:dump-reference FrameworkBundle http_client.default_options to dump the framework.http_client.default_options configuration)', ]); return 0; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php index 01394ff70179d..df71b38b2faa7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php @@ -361,7 +361,7 @@ private function getCallableData(mixed $callable): array } $data['name'] = $r->name; - if ($class = $r->getClosureScopeClass()) { + if ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { $data['class'] = $class->name; if (!$r->getClosureThis()) { $data['static'] = true; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php index 810104e0e5c9d..22cfe9259292f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php @@ -377,7 +377,7 @@ protected function describeCallable(mixed $callable, array $options = []) } $string .= "\n".sprintf('- Name: `%s`', $r->name); - if ($class = $r->getClosureScopeClass()) { + if ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { $string .= "\n".sprintf('- Class: `%s`', $class->name); if (!$r->getClosureThis()) { $string .= "\n- Static: yes"; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php index 1b31fe91af45b..df1b141cfa3f1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php @@ -614,7 +614,7 @@ private function formatCallable(mixed $callable): string if (str_contains($r->name, '{closure}')) { return 'Closure()'; } - if ($class = $r->getClosureScopeClass()) { + if ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { return sprintf('%s::%s()', $class->name, $r->name); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php index d7187614e8d2c..bdc52c45e134a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php @@ -548,7 +548,7 @@ private function getCallableDocument(mixed $callable): \DOMDocument } $callableXML->setAttribute('name', $r->name); - if ($class = $r->getClosureScopeClass()) { + if ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { $callableXML->setAttribute('class', $class->name); if (!$r->getClosureThis()) { $callableXML->setAttribute('static', 'true'); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 2decf5b40c4e6..28f3b07adbf78 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1185,35 +1185,31 @@ private function addExceptionsSection(ArrayNodeDefinition $rootNode) $logLevels = (new \ReflectionClass(LogLevel::class))->getConstants(); $rootNode + ->fixXmlConfig('exception') ->children() ->arrayNode('exceptions') ->info('Exception handling configuration') + ->useAttributeAsKey('class') ->beforeNormalization() + // Handle legacy XML configuration ->ifArray() ->then(function (array $v): array { if (!\array_key_exists('exception', $v)) { return $v; } - // Fix XML normalization - $data = isset($v['exception'][0]) ? $v['exception'] : [$v['exception']]; - $exceptions = []; - foreach ($data as $exception) { - $config = []; - if (\array_key_exists('log-level', $exception)) { - $config['log_level'] = $exception['log-level']; - } - if (\array_key_exists('status-code', $exception)) { - $config['status_code'] = $exception['status-code']; - } - $exceptions[$exception['name']] = $config; + $v = $v['exception']; + unset($v['exception']); + + foreach ($v as &$exception) { + $exception['class'] = $exception['name']; + unset($exception['name']); } - return $exceptions; + return $v; }) ->end() ->prototype('array') - ->fixXmlConfig('exception') ->children() ->scalarNode('log_level') ->info('The level of log message. Null to let Symfony decide.') diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 788d8929627ec..06137d9508c45 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1646,8 +1646,12 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde $loader->load('annotations.php'); if (!method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { - $container->getDefinition('annotations.dummy_registry') - ->setMethodCalls([['registerLoader', ['class_exists']]]); + if (method_exists(AnnotationRegistry::class, 'registerLoader')) { + $container->getDefinition('annotations.dummy_registry') + ->setMethodCalls([['registerLoader', ['class_exists']]]); + } else { + $container->removeDefinition('annotations.dummy_registry'); + } } if ('none' === $config['cache']) { @@ -2738,10 +2742,14 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil // Settings $def->addMethodCall('forceHttpsUrls', [$sanitizerConfig['force_https_urls']], true); - $def->addMethodCall('allowLinkSchemes', [$sanitizerConfig['allowed_link_schemes']], true); + if ($sanitizerConfig['allowed_link_schemes']) { + $def->addMethodCall('allowLinkSchemes', [$sanitizerConfig['allowed_link_schemes']], true); + } $def->addMethodCall('allowLinkHosts', [$sanitizerConfig['allowed_link_hosts']], true); $def->addMethodCall('allowRelativeLinks', [$sanitizerConfig['allow_relative_links']], true); - $def->addMethodCall('allowMediaSchemes', [$sanitizerConfig['allowed_media_schemes']], true); + if ($sanitizerConfig['allowed_media_schemes']) { + $def->addMethodCall('allowMediaSchemes', [$sanitizerConfig['allowed_media_schemes']], true); + } $def->addMethodCall('allowMediaHosts', [$sanitizerConfig['allowed_media_hosts']], true); $def->addMethodCall('allowRelativeMedias', [$sanitizerConfig['allow_relative_medias']], true); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php index 1b178f16a95d3..65a3f4e8ffd90 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php @@ -25,7 +25,7 @@ ->set('annotations.reader', AnnotationReader::class) ->call('addGlobalIgnoredName', [ 'required', - service('annotations.dummy_registry'), // dummy arg to register class_exists as annotation loader only when required + service('annotations.dummy_registry')->ignoreOnInvalid(), // dummy arg to register class_exists as annotation loader only when required ]) ->set('annotations.dummy_registry', AnnotationRegistry::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index cd53d88db00ac..242c65e56a908 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -30,6 +30,7 @@ + @@ -364,14 +365,29 @@ - + - - + - + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php index fadaff6d2b596..e40f1aae4d619 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php @@ -168,10 +168,11 @@ ->set('translation.locale_switcher', LocaleSwitcher::class) ->args([ param('kernel.default_locale'), - tagged_iterator('kernel.locale_aware'), + tagged_iterator('kernel.locale_aware', exclude: 'translation.locale_switcher'), service('router.request_context')->ignoreOnInvalid(), ]) ->tag('kernel.reset', ['method' => 'reset']) + ->tag('kernel.locale_aware') ->alias(LocaleAwareInterface::class, 'translation.locale_switcher') ->alias(LocaleSwitcher::class, 'translation.locale_switcher') ; diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php index 1a629d6255fbe..d6b29d2b5a0c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php @@ -123,10 +123,6 @@ private static function getMessageMailerEvents(): MessageEvents return $container->get('mailer.message_logger_listener')->getEvents(); } - if ($container->has('mailer.logger_message_listener')) { - return $container->get('mailer.logger_message_listener')->getEvents(); - } - static::fail('A client must have Mailer enabled to make email assertions. Did you forget to require symfony/mailer?'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml index 2e6048fa7c7aa..4a877c1eb3499 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml @@ -6,11 +6,25 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - - - - - + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions_legacy.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions_legacy.xml new file mode 100644 index 0000000000000..2e6048fa7c7aa --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions_legacy.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 3743acfe915f9..6297172706661 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -37,6 +37,7 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveTaggedIteratorArgumentPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; @@ -575,24 +576,34 @@ public function testExceptionsConfig() { $container = $this->createContainerFromFile('exceptions'); + $configuration = $container->getDefinition('exception_listener')->getArgument(3); + $this->assertSame([ - \Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class => [ - 'log_level' => 'info', - 'status_code' => 422, - ], - \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class => [ - 'log_level' => 'info', - 'status_code' => null, - ], - \Symfony\Component\HttpKernel\Exception\ConflictHttpException::class => [ - 'log_level' => 'info', - 'status_code' => null, - ], - \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class => [ - 'log_level' => null, - 'status_code' => 500, - ], - ], $container->getDefinition('exception_listener')->getArgument(3)); + \Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class, + \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class, + \Symfony\Component\HttpKernel\Exception\ConflictHttpException::class, + \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class, + ], array_keys($configuration)); + + $this->assertEqualsCanonicalizing([ + 'log_level' => 'info', + 'status_code' => 422, + ], $configuration[\Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class]); + + $this->assertEqualsCanonicalizing([ + 'log_level' => 'info', + 'status_code' => null, + ], $configuration[\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class]); + + $this->assertEqualsCanonicalizing([ + 'log_level' => 'info', + 'status_code' => null, + ], $configuration[\Symfony\Component\HttpKernel\Exception\ConflictHttpException::class]); + + $this->assertEqualsCanonicalizing([ + 'log_level' => null, + 'status_code' => 500, + ], $configuration[\Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class]); } public function testRouter() @@ -2057,7 +2068,9 @@ public function testLocaleSwitcherServiceRegistered() $this->markTestSkipped('LocaleSwitcher not available.'); } - $container = $this->createContainerFromFile('full'); + $container = $this->createContainerFromFile('full', compile: false); + $container->addCompilerPass(new ResolveTaggedIteratorArgumentPass()); + $container->compile(); $this->assertTrue($container->has('translation.locale_switcher')); @@ -2067,6 +2080,10 @@ public function testLocaleSwitcherServiceRegistered() $this->assertInstanceOf(TaggedIteratorArgument::class, $switcherDef->getArgument(1)); $this->assertSame('kernel.locale_aware', $switcherDef->getArgument(1)->getTag()); $this->assertEquals(new Reference('router.request_context', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE), $switcherDef->getArgument(2)); + + $localeAwareServices = array_map(fn (Reference $r) => (string) $r, $switcherDef->getArgument(1)->getValues()); + + $this->assertNotContains('translation.locale_switcher', $localeAwareServices); } public function testHtmlSanitizer() @@ -2133,7 +2150,9 @@ public function testHtmlSanitizerDefaultNullAllowedLinkMediaHost() $calls = $container->getDefinition('html_sanitizer.config.custom_default')->getMethodCalls(); $this->assertContains(['allowLinkHosts', [null], true], $calls); + $this->assertContains(['allowRelativeLinks', [false], true], $calls); $this->assertContains(['allowMediaHosts', [null], true], $calls); + $this->assertContains(['allowRelativeMedias', [false], true], $calls); } public function testHtmlSanitizerDefaultConfig() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php index ebc37d93bed84..131bb07f0c657 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php @@ -32,4 +32,38 @@ public function testMessengerMiddlewareFactoryErroneousFormat() { $this->markTestSkipped('XML configuration will not allow erroneous format.'); } + + public function testLegacyExceptionsConfig() + { + $container = $this->createContainerFromFile('exceptions_legacy'); + + $configuration = $container->getDefinition('exception_listener')->getArgument(3); + + $this->assertSame([ + \Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class, + \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class, + \Symfony\Component\HttpKernel\Exception\ConflictHttpException::class, + \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class, + ], array_keys($configuration)); + + $this->assertEqualsCanonicalizing([ + 'log_level' => 'info', + 'status_code' => 422, + ], $configuration[\Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class]); + + $this->assertEqualsCanonicalizing([ + 'log_level' => 'info', + 'status_code' => null, + ], $configuration[\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class]); + + $this->assertEqualsCanonicalizing([ + 'log_level' => 'info', + 'status_code' => null, + ], $configuration[\Symfony\Component\HttpKernel\Exception\ConflictHttpException::class]); + + $this->assertEqualsCanonicalizing([ + 'log_level' => null, + 'status_code' => 500, + ], $configuration[\Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class]); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index dc1e717e28595..f80271f1dcfd4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -33,7 +33,7 @@ "symfony/routing": "^5.4|^6.0" }, "require-dev": { - "doctrine/annotations": "^1.13.1", + "doctrine/annotations": "^1.13.1|^2", "doctrine/persistence": "^1.3|^2|^3", "symfony/asset": "^5.4|^6.0", "symfony/browser-kit": "^5.4|^6.0", diff --git a/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php index ea1e44018ea05..dedcda9bd6f6f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php @@ -245,7 +245,7 @@ private function formatCallable(mixed $callable): string if (str_contains($r->name, '{closure}')) { return 'Closure()'; } - if ($class = $r->getClosureScopeClass()) { + if ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { return sprintf('%s::%s()', $class->name, $r->name); } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 9c3db8d752dc6..76fe1e4efc770 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -30,7 +30,7 @@ "symfony/security-http": "^5.4|^6.0" }, "require-dev": { - "doctrine/annotations": "^1.10.4", + "doctrine/annotations": "^1.10.4|^2", "symfony/asset": "^5.4|^6.0", "symfony/browser-kit": "^5.4|^6.0", "symfony/console": "^5.4|^6.0", diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index 379d95c2fd3cb..b7c2c723bcc6f 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -37,7 +37,7 @@ "symfony/yaml": "^5.4|^6.0", "symfony/framework-bundle": "^5.4|^6.0", "symfony/web-link": "^5.4|^6.0", - "doctrine/annotations": "^1.10.4" + "doctrine/annotations": "^1.10.4|^2" }, "conflict": { "symfony/framework-bundle": "<5.4", diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig index 5246dc1049f0f..c8f787c2a62d8 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig @@ -253,10 +253,6 @@ {% if has_trace %} {% set trace_id = 'trace-' ~ category ~ '-' ~ log_index %} - -
- {{ profiler_dump(log.context.exception.trace, maxDepth=1) }} -
{% endif %} {% if has_context %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php index fbaf2f7965d05..c345b5fbb89c8 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php @@ -66,10 +66,10 @@ public function provideRequestAndResponses() ]; return [ - [$nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], $this->createRequest(), $this->createResponse()], - [$nonce, ['csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce], $this->createRequest($requestNonceHeaders), $this->createResponse($responseNonceHeaders)], - [$nonce, ['csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce], $this->createRequest($requestNonceHeaders), $this->createResponse()], - [$nonce, ['csp_script_nonce' => $responseScriptNonce, 'csp_style_nonce' => $responseStyleNonce], $this->createRequest(), $this->createResponse($responseNonceHeaders)], + [$nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], self::createRequest(), self::createResponse()], + [$nonce, ['csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce], self::createRequest($requestNonceHeaders), self::createResponse($responseNonceHeaders)], + [$nonce, ['csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce], self::createRequest($requestNonceHeaders), self::createResponse()], + [$nonce, ['csp_script_nonce' => $responseScriptNonce, 'csp_style_nonce' => $responseStyleNonce], self::createRequest(), self::createResponse($responseNonceHeaders)], ]; } @@ -96,104 +96,104 @@ public function provideRequestAndResponsesForOnKernelResponse() [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(), + self::createRequest(), + self::createResponse(), ['Content-Security-Policy' => null, 'Content-Security-Policy-Report-Only' => null, 'X-Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce], - $this->createRequest($requestNonceHeaders), - $this->createResponse($responseNonceHeaders), + self::createRequest($requestNonceHeaders), + self::createResponse($responseNonceHeaders), ['Content-Security-Policy' => null, 'Content-Security-Policy-Report-Only' => null, 'X-Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce], - $this->createRequest($requestNonceHeaders), - $this->createResponse(), + self::createRequest($requestNonceHeaders), + self::createResponse(), ['Content-Security-Policy' => null, 'Content-Security-Policy-Report-Only' => null, 'X-Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $responseScriptNonce, 'csp_style_nonce' => $responseStyleNonce], - $this->createRequest(), - $this->createResponse($responseNonceHeaders), + self::createRequest(), + self::createResponse($responseNonceHeaders), ['Content-Security-Policy' => null, 'Content-Security-Policy-Report-Only' => null, 'X-Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(['Content-Security-Policy' => 'frame-ancestors https: ; form-action: https:', 'Content-Security-Policy-Report-Only' => 'frame-ancestors http: ; form-action: http:']), + self::createRequest(), + self::createResponse(['Content-Security-Policy' => 'frame-ancestors https: ; form-action: https:', 'Content-Security-Policy-Report-Only' => 'frame-ancestors http: ; form-action: http:']), ['Content-Security-Policy' => 'frame-ancestors https: ; form-action: https:', 'Content-Security-Policy-Report-Only' => 'frame-ancestors http: ; form-action: http:', 'X-Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'']), + self::createRequest(), + self::createResponse(['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'']), ['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; style-src \'self\' domain.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; style-src \'self\' domain-report-only.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\'']), + self::createRequest(), + self::createResponse(['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\'']), ['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(['Content-Security-Policy' => 'default-src \'none\'', 'Content-Security-Policy-Report-Only' => 'default-src \'none\'']), + self::createRequest(), + self::createResponse(['Content-Security-Policy' => 'default-src \'none\'', 'Content-Security-Policy-Report-Only' => 'default-src \'none\'']), ['Content-Security-Policy' => 'default-src \'none\'; script-src \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy-Report-Only' => 'default-src \'none\'; script-src \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(['Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'']), + self::createRequest(), + self::createResponse(['Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'']), ['Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'', 'X-Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(['Content-Security-Policy' => 'script-src \'self\'; style-src \'self\'']), + self::createRequest(), + self::createResponse(['Content-Security-Policy' => 'script-src \'self\'; style-src \'self\'']), ['Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(['X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'']), + self::createRequest(), + self::createResponse(['X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'']), ['X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'', 'Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(['X-Content-Security-Policy' => 'script-src \'self\'']), + self::createRequest(), + self::createResponse(['X-Content-Security-Policy' => 'script-src \'self\'']), ['X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(['X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'sha384-LALALALALAAL\'']), + self::createRequest(), + self::createResponse(['X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'sha384-LALALALALAAL\'']), ['X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'sha384-LALALALALAAL\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy' => null], ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], - $this->createRequest(), - $this->createResponse(['Content-Security-Policy' => 'script-src \'self\'; style-src \'self\'', 'X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'; style-src \'self\'']), + self::createRequest(), + self::createResponse(['Content-Security-Policy' => 'script-src \'self\'; style-src \'self\'', 'X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'; style-src \'self\'']), ['Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\''], ], ]; } - private function createRequest(array $headers = []) + private static function createRequest(array $headers = []) { $request = new Request(); $request->headers->add($headers); @@ -201,7 +201,7 @@ private function createRequest(array $headers = []) return $request; } - private function createResponse(array $headers = []) + private static function createResponse(array $headers = []) { $response = new Response(); $response->headers->add($headers); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php index 0038736820388..fbfd2ed586f87 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php @@ -33,8 +33,12 @@ class WebProfilerExtensionTest extends TestCase public static function assertSaneContainer(Container $container) { + $removedIds = $container->getRemovedIds(); $errors = []; foreach ($container->getServiceIds() as $id) { + if (isset($removedIds[$id])) { + continue; + } try { $container->get($id); } catch (\Exception $e) { diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php new file mode 100644 index 0000000000000..6b026bcc53385 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Tests\Twig; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Twig\Environment; +use Twig\Extension\CoreExtension; +use Twig\Extension\EscaperExtension; + +class WebProfilerExtensionTest extends TestCase +{ + /** + * @dataProvider provideMessages + */ + public function testDumpHeaderIsDisplayed(string $message, array $context, bool $dump1HasHeader, bool $dump2HasHeader) + { + class_exists(CoreExtension::class); // Load twig_convert_encoding() + class_exists(EscaperExtension::class); // Load twig_escape_filter() + + $twigEnvironment = $this->mockTwigEnvironment(); + $varCloner = new VarCloner(); + + $webProfilerExtension = new WebProfilerExtension(); + + $needle = 'window.Sfdump'; + + $dump1 = $webProfilerExtension->dumpLog($twigEnvironment, $message, $varCloner->cloneVar($context)); + self::assertSame($dump1HasHeader, str_contains($dump1, $needle)); + + $dump2 = $webProfilerExtension->dumpData($twigEnvironment, $varCloner->cloneVar([])); + self::assertSame($dump2HasHeader, str_contains($dump2, $needle)); + } + + public function provideMessages(): iterable + { + yield ['Some message', ['foo' => 'foo', 'bar' => 'bar'], false, true]; + yield ['Some message {@see some text}', ['foo' => 'foo', 'bar' => 'bar'], false, true]; + yield ['Some message {foo}', ['foo' => 'foo', 'bar' => 'bar'], true, false]; + yield ['Some message {foo}', ['bar' => 'bar'], false, true]; + } + + private function mockTwigEnvironment() + { + $twigEnvironment = $this->createMock(Environment::class); + + $twigEnvironment->expects($this->any())->method('getCharset')->willReturn('UTF-8'); + + return $twigEnvironment; + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php index c29b4c4f52911..84dd1a44728ef 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php @@ -90,13 +90,19 @@ public function dumpLog(Environment $env, string $message, Data $context = null) $message = twig_escape_filter($env, $message); $message = preg_replace('/"(.*?)"/', '"$1"', $message); - if (null === $context || !str_contains($message, '{')) { + $replacements = []; + foreach ($context ?? [] as $k => $v) { + $k = '{'.twig_escape_filter($env, $k).'}'; + if (str_contains($message, $k)) { + $replacements[$k] = $v; + } + } + + if (!$replacements) { return ''.$message.''; } - $replacements = []; - foreach ($context as $k => $v) { - $k = '{'.twig_escape_filter($env, $k).'}'; + foreach ($replacements as $k => $v) { $replacements['"'.$k.'"'] = $replacements['"'.$k.'"'] = $replacements[$k] = $this->dumpData($env, $v); } diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index c745deb6e4618..e531aa0ef4ee8 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -202,7 +202,7 @@ public static function createConnection(string $dsn, array $options = []): \Redi break; } - $sentinel = new \RedisSentinel($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout']); + $sentinel = new \RedisSentinel($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...\defined('Redis::OPT_NULL_MULTIBULK_AS_NULL') ? [$params['auth'] ?? ''] : []); if ($address = $sentinel->getMasterAddrByName($params['redis_sentinel'])) { [$host, $port] = $address; @@ -214,7 +214,10 @@ public static function createConnection(string $dsn, array $options = []): \Redi } try { - @$redis->{$connect}($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...\defined('Redis::SCAN_PREFIX') ? [['stream' => $params['ssl'] ?? null]] : []); + @$redis->{$connect}($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...\defined('Redis::SCAN_PREFIX') ? [[ + 'auth' => $params['auth'] ?? '', + 'stream' => $params['ssl'] ?? null, + ]] : []); set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); try { @@ -568,7 +571,11 @@ private function pipeline(\Closure $generator, object $redis = null): \Generator if (!$redis instanceof \Predis\ClientInterface && 'eval' === $command && $redis->getLastError()) { $e = new \RedisException($redis->getLastError()); - $results = array_map(function ($v) use ($e) { return false === $v ? $e : $v; }, $results); + $results = array_map(function ($v) use ($e) { return false === $v ? $e : $v; }, (array) $results); + } + + if (\is_bool($results)) { + return; } foreach ($ids as $k => $id) { diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 670e07d555fd0..0ec2a14f1447a 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -53,7 +53,6 @@ final class ProgressBar private int $startTime; private int $stepWidth; private float $percent = 0.0; - private int $formatLineCount; private array $messages = []; private bool $overwrite = true; private Terminal $terminal; @@ -438,8 +437,6 @@ private function setRealFormat(string $format) } else { $this->format = $format; } - - $this->formatLineCount = substr_count($this->format, "\n"); } /** @@ -456,7 +453,7 @@ private function overwrite(string $message): void if ($this->overwrite) { if (null !== $this->previousMessage) { if ($this->output instanceof ConsoleSectionOutput) { - $messageLines = explode("\n", $message); + $messageLines = explode("\n", $this->previousMessage); $lineCount = \count($messageLines); foreach ($messageLines as $messageLine) { $messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine)); @@ -466,13 +463,11 @@ private function overwrite(string $message): void } $this->output->clear($lineCount); } else { - if ('' !== $this->previousMessage) { - // only clear upper lines when last call was not a clear - for ($i = 0; $i < $this->formatLineCount; ++$i) { - $this->cursor->moveToColumn(1); - $this->cursor->clearLine(); - $this->cursor->moveUp(); - } + $lineCount = substr_count($this->previousMessage, "\n"); + for ($i = 0; $i < $lineCount; ++$i) { + $this->cursor->moveToColumn(1); + $this->cursor->clearLine(); + $this->cursor->moveUp(); } $this->cursor->moveToColumn(1); diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 3a85b2d4abae5..0f96b4898dfbd 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -1969,6 +1969,10 @@ public function testSetSignalsToDispatchEvent() public function testSignalableCommandInterfaceWithoutSignals() { + if (!\defined('SIGUSR1')) { + $this->markTestSkipped('SIGUSR1 not available'); + } + $command = new SignableCommand(false); $dispatcher = new EventDispatcher(); diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 17401b887ec5f..c9b9c9d535956 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -361,8 +361,8 @@ public function testOverwriteWithAnsiSectionOutput() rewind($output->getStream()); $this->assertSame( " \033[44;37m 0/50\033[0m [>---------------------------] 0%".\PHP_EOL. - "\x1b[1A\x1b[0J"." \033[44;37m 1/50\033[0m [>---------------------------] 2%".\PHP_EOL. - "\x1b[1A\x1b[0J"." \033[44;37m 2/50\033[0m [=>--------------------------] 4%".\PHP_EOL, + "\x1b[1A\x1b[0J \033[44;37m 1/50\033[0m [>---------------------------] 2%".\PHP_EOL. + "\x1b[1A\x1b[0J \033[44;37m 2/50\033[0m [=>--------------------------] 4%".\PHP_EOL, stream_get_contents($output->getStream()) ); putenv('COLUMNS=120'); @@ -397,6 +397,28 @@ public function testOverwriteMultipleProgressBarsWithSectionOutputs() ); } + public function testOverwritWithNewlinesInMessage() + { + ProgressBar::setFormatDefinition('test', '%current%/%max% [%bar%] %percent:3s%% %message% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'); + + $bar = new ProgressBar($output = $this->getOutputStream(), 50, 0); + $bar->setFormat('test'); + $bar->start(); + $bar->display(); + $bar->setMessage("Twas brillig, and the slithy toves. Did gyre and gimble in the wabe: All mimsy were the borogoves, And the mome raths outgrabe.\nBeware the Jabberwock, my son! The jaws that bite, the claws that catch! Beware the Jubjub bird, and shun The frumious Bandersnatch!"); + $bar->advance(); + $bar->setMessage("He took his vorpal sword in hand; Long time the manxome foe he sought— So rested he by the Tumtum tree And stood awhile in thought.\nAnd, as in uffish thought he stood, The Jabberwock, with eyes of flame, Came whiffling through the tulgey wood, And burbled as it came!"); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + " 0/50 [>] 0% %message% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.\x1b[1G\x1b[2K 1/50 [>] 2% Twas brillig, and the slithy toves. Did gyre and gimble in the wabe: All mimsy were the borogoves, And the mome raths outgrabe. +Beware the Jabberwock, my son! The jaws that bite, the claws that catch! Beware the Jubjub bird, and shun The frumious Bandersnatch! Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.\x1b[1G\x1b[2K\x1b[1A\x1b[1G\x1b[2K 2/50 [>] 4% He took his vorpal sword in hand; Long time the manxome foe he sought— So rested he by the Tumtum tree And stood awhile in thought. +And, as in uffish thought he stood, The Jabberwock, with eyes of flame, Came whiffling through the tulgey wood, And burbled as it came! Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.", + stream_get_contents($output->getStream()) + ); + } + public function testOverwriteWithSectionOutputWithNewlinesInMessage() { $sections = []; @@ -417,7 +439,7 @@ public function testOverwriteWithSectionOutputWithNewlinesInMessage() rewind($output->getStream()); $this->assertEquals( ' 0/50 [>] 0% %message% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'.\PHP_EOL. - "\x1b[6A\x1b[0J 1/50 [>] 2% Twas brillig, and the slithy toves. Did gyre and gimble in the wabe: All mimsy were the borogoves, And the mome raths outgrabe. + "\x1b[3A\x1b[0J 1/50 [>] 2% Twas brillig, and the slithy toves. Did gyre and gimble in the wabe: All mimsy were the borogoves, And the mome raths outgrabe. Beware the Jabberwock, my son! The jaws that bite, the claws that catch! Beware the Jubjub bird, and shun The frumious Bandersnatch! Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.".\PHP_EOL. "\x1b[6A\x1b[0J 2/50 [>] 4% He took his vorpal sword in hand; Long time the manxome foe he sought— So rested he by the Tumtum tree And stood awhile in thought. And, as in uffish thought he stood, The Jabberwock, with eyes of flame, Came whiffling through the tulgey wood, And burbled as it came! Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.".\PHP_EOL, diff --git a/src/Symfony/Component/CssSelector/Parser/Tokenizer/TokenizerPatterns.php b/src/Symfony/Component/CssSelector/Parser/Tokenizer/TokenizerPatterns.php index 0b2767a8e8088..3c77cf091c6fd 100644 --- a/src/Symfony/Component/CssSelector/Parser/Tokenizer/TokenizerPatterns.php +++ b/src/Symfony/Component/CssSelector/Parser/Tokenizer/TokenizerPatterns.php @@ -49,22 +49,22 @@ public function __construct() $this->identifierPattern = '-?(?:'.$this->nmStartPattern.')(?:'.$this->nmCharPattern.')*'; $this->hashPattern = '#((?:'.$this->nmCharPattern.')+)'; $this->numberPattern = '[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)'; - $this->quotedStringPattern = '([^\n\r\f%s]|'.$this->stringEscapePattern.')*'; + $this->quotedStringPattern = '([^\n\r\f\\\\%s]|'.$this->stringEscapePattern.')*'; } public function getNewLineEscapePattern(): string { - return '~^'.$this->newLineEscapePattern.'~'; + return '~'.$this->newLineEscapePattern.'~'; } public function getSimpleEscapePattern(): string { - return '~^'.$this->simpleEscapePattern.'~'; + return '~'.$this->simpleEscapePattern.'~'; } public function getUnicodeEscapePattern(): string { - return '~^'.$this->unicodeEscapePattern.'~i'; + return '~'.$this->unicodeEscapePattern.'~i'; } public function getIdentifierPattern(): string diff --git a/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php b/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php index 48a67f5ab6678..77ce5d58258a2 100644 --- a/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php +++ b/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php @@ -138,6 +138,16 @@ public function getParserTestData() ['div:not(div.foo)', ['Negation[Element[div]:not(Class[Element[div].foo])]']], ['td ~ th', ['CombinedSelector[Element[td] ~ Element[th]]']], ['.foo[data-bar][data-baz=0]', ["Attribute[Attribute[Class[Element[*].foo][data-bar]][data-baz = '0']]"]], + ['div#foo\.bar', ['Hash[Element[div]#foo.bar]']], + ['div.w-1\/3', ['Class[Element[div].w-1/3]']], + ['#test\:colon', ['Hash[Element[*]#test:colon]']], + [".a\xc1b", ["Class[Element[*].a\xc1b]"]], + // unicode escape: \22 == " + ['*[aval="\'\22\'"]', ['Attribute[Element[*][aval = \'\'"\'\']]']], + ['*[aval="\'\22 2\'"]', ['Attribute[Element[*][aval = \'\'"2\'\']]']], + // unicode escape: \20 == (space) + ['*[aval="\'\20 \'"]', ['Attribute[Element[*][aval = \'\' \'\']]']], + ["*[aval=\"'\\20\r\n '\"]", ['Attribute[Element[*][aval = \'\' \'\']]']], ]; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php index e46ac5dbb4146..a6f5187782f74 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php @@ -129,7 +129,7 @@ private function processDefinition(ContainerBuilder $container, string $id, Defi foreach ($instanceofTags[$i] as $k => $v) { if (null === $definition->getDecoratedService() || \in_array($k, $tagsToKeep, true)) { foreach ($v as $v) { - if ($definition->hasTag($k) && (!$v || \in_array($v, $definition->getTag($k)))) { + if ($definition->hasTag($k) && \in_array($v, $definition->getTag($k))) { continue; } $definition->addTag($k, $v); diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 853b0e964ba2f..849e8a8404d80 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -505,8 +505,8 @@ public function has(string $id): bool */ public function get(string $id, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): ?object { - if ($this->isCompiled() && isset($this->removedIds[$id]) && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE >= $invalidBehavior) { - return parent::get($id); + if ($this->isCompiled() && isset($this->removedIds[$id])) { + return ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE >= $invalidBehavior ? parent::get($id) : null; } return $this->doGet($id, $invalidBehavior); @@ -523,9 +523,9 @@ private function doGet(string $id, int $invalidBehavior = ContainerInterface::EX } try { if (ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $invalidBehavior) { - return parent::get($id, $invalidBehavior); + return $this->privates[$id] ?? parent::get($id, $invalidBehavior); } - if ($service = parent::get($id, ContainerInterface::NULL_ON_INVALID_REFERENCE)) { + if (null !== $service = $this->privates[$id] ?? parent::get($id, ContainerInterface::NULL_ON_INVALID_REFERENCE)) { return $service; } } catch (ServiceCircularReferenceException $e) { @@ -1025,8 +1025,8 @@ private function createService(Definition $definition, array &$inlineServices, b $arguments = $this->doResolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($arguments)), $inlineServices, $isConstructorArgument); - if (null !== $id && $definition->isShared() && isset($this->services[$id]) && ($tryProxy || !$definition->isLazy())) { - return $this->services[$id]; + if (null !== $id && $definition->isShared() && (isset($this->services[$id]) || isset($this->privates[$id])) && ($tryProxy || !$definition->isLazy())) { + return $this->services[$id] ?? $this->privates[$id]; } if (null !== $factory) { @@ -1580,7 +1580,11 @@ private function shareService(Definition $definition, mixed $service, ?string $i $inlineServices[$id ?? spl_object_hash($definition)] = $service; if (null !== $id && $definition->isShared()) { - $this->services[$id] = $service; + if ($definition->isPrivate() && $this->isCompiled()) { + $this->privates[$id] = $service; + } else { + $this->services[$id] = $service; + } unset($this->loading[$id]); } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index d3c8ebdb98873..4cdb4ba2019c9 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -32,7 +32,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\ExpressionLanguage; -use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface as ProxyDumper; +use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper; use Symfony\Component\DependencyInjection\Loader\FileLoader; use Symfony\Component\DependencyInjection\Parameter; @@ -90,7 +90,8 @@ class PhpDumper extends Dumper private string $serviceLocatorTag; private array $exportedVariables = []; private string $baseClass; - private ProxyDumper $proxyDumper; + private DumperInterface $proxyDumper; + private bool $hasProxyDumper = false; /** * {@inheritdoc} @@ -107,9 +108,10 @@ public function __construct(ContainerBuilder $container) /** * Sets the dumper to be used when dumping proxies in the generated container. */ - public function setProxyDumper(ProxyDumper $proxyDumper) + public function setProxyDumper(DumperInterface $proxyDumper) { $this->proxyDumper = $proxyDumper; + $this->hasProxyDumper = !$proxyDumper instanceof NullDumper; } /** @@ -167,7 +169,7 @@ public function dump(array $options = []): string|array $this->initializeMethodNamesMap('Container' === $baseClass ? Container::class : $baseClass); - if ($this->getProxyDumper() instanceof NullDumper) { + if (!$this->hasProxyDumper) { (new AnalyzeServiceReferencesPass(true, false))->process($this->container); try { (new CheckCircularReferencesPass())->process($this->container); @@ -400,14 +402,14 @@ class %s extends {$options['class']} /** * Retrieves the currently set proxy dumper or instantiates one. */ - private function getProxyDumper(): ProxyDumper + private function getProxyDumper(): DumperInterface { return $this->proxyDumper ??= new NullDumper(); } private function analyzeReferences() { - (new AnalyzeServiceReferencesPass(false, !$this->getProxyDumper() instanceof NullDumper))->process($this->container); + (new AnalyzeServiceReferencesPass(false, $this->hasProxyDumper))->process($this->container); $checkedNodes = []; $this->circularReferences = []; $this->singleUsePrivateIds = []; @@ -434,13 +436,13 @@ private function collectCircularReferences(string $sourceId, array $edges, array foreach ($edges as $edge) { $node = $edge->getDestNode(); $id = $node->getId(); - if ($sourceId === $id || !$node->getValue() instanceof Definition || $edge->isLazy() || $edge->isWeak()) { + if ($sourceId === $id || !$node->getValue() instanceof Definition || $edge->isWeak()) { continue; } if (isset($path[$id])) { $loop = null; - $loopByConstructor = $edge->isReferencedByConstructor(); + $loopByConstructor = $edge->isReferencedByConstructor() && !$edge->isLazy(); $pathInLoop = [$id, []]; foreach ($path as $k => $pathByConstructor) { if (null !== $loop) { @@ -454,7 +456,7 @@ private function collectCircularReferences(string $sourceId, array $edges, array } $this->addCircularReferences($id, $loop, $loopByConstructor); } elseif (!isset($checkedNodes[$id])) { - $this->collectCircularReferences($id, $node->getOutEdges(), $checkedNodes, $loops, $path, $edge->isReferencedByConstructor()); + $this->collectCircularReferences($id, $node->getOutEdges(), $checkedNodes, $loops, $path, $edge->isReferencedByConstructor() && !$edge->isLazy()); } elseif (isset($loops[$id])) { // we already had detected loops for this edge // let's check if we have a common ancestor in one of the detected loops @@ -475,7 +477,7 @@ private function collectCircularReferences(string $sourceId, array $edges, array // we can now build the loop $loop = null; - $loopByConstructor = $edge->isReferencedByConstructor(); + $loopByConstructor = $edge->isReferencedByConstructor() && !$edge->isLazy(); foreach ($fillPath as $k => $pathByConstructor) { if (null !== $loop) { $loop[] = $k; @@ -975,7 +977,7 @@ private function addInlineReference(string $id, Definition $definition, string $ return ''; } - $hasSelfRef = isset($this->circularReferences[$id][$targetId]) && !isset($this->definitionVariables[$definition]); + $hasSelfRef = isset($this->circularReferences[$id][$targetId]) && !isset($this->definitionVariables[$definition]) && !($this->hasProxyDumper && $definition->isLazy()); if ($hasSelfRef && !$forConstructor && !$forConstructor = !$this->circularReferences[$id][$targetId]) { $code = $this->addInlineService($id, $definition, $definition); @@ -1018,7 +1020,7 @@ private function addInlineService(string $id, Definition $definition, Definition if ($isSimpleInstance = $isRootInstance = null === $inlineDef) { foreach ($this->serviceCalls as $targetId => [$callCount, $behavior, $byConstructor]) { - if ($byConstructor && isset($this->circularReferences[$id][$targetId]) && !$this->circularReferences[$id][$targetId]) { + if ($byConstructor && isset($this->circularReferences[$id][$targetId]) && !$this->circularReferences[$id][$targetId] && !($this->hasProxyDumper && $definition->isLazy())) { $code .= $this->addInlineReference($id, $definition, $targetId, $forConstructor); } } diff --git a/src/Symfony/Component/DependencyInjection/ReverseContainer.php b/src/Symfony/Component/DependencyInjection/ReverseContainer.php index 9635c5bceefe5..9b1d331d85e63 100644 --- a/src/Symfony/Component/DependencyInjection/ReverseContainer.php +++ b/src/Symfony/Component/DependencyInjection/ReverseContainer.php @@ -63,10 +63,6 @@ public function getId(object $service): ?string */ public function getService(string $id): object { - if ($this->serviceContainer->has($id)) { - return $this->serviceContainer->get($id); - } - if ($this->reversibleLocator->has($id)) { return $this->reversibleLocator->get($id); } @@ -75,7 +71,6 @@ public function getService(string $id): object throw new ServiceNotFoundException($id, null, null, [], sprintf('The "%s" service is private and cannot be accessed by reference. You should either make it public, or tag it as "%s".', $id, $this->tagName)); } - // will throw a ServiceNotFoundException - $this->serviceContainer->get($id); + return $this->serviceContainer->get($id); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php index 72f875db6903d..e7fcac4623f84 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php @@ -849,7 +849,6 @@ static function (ChildDefinition $definition, CustomAutoconfiguration $attribute $definition->addTag('app.custom_tag', get_object_vars($attribute) + ['class' => $reflector->getName()]); } ); - $container->registerForAutoconfiguration(TaggedService1::class)->addTag('app.custom_tag'); $container->register('one', TaggedService1::class) ->setPublic(true) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php index 4d248306a0b7d..6ddbc88d8ee8c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php @@ -184,19 +184,19 @@ public function testIndexedByServiceIdWithDecoration() $container->setDefinition(Service::class, $service); - $decorated = new Definition(Decorated::class); + $decorated = new Definition(DecoratedService::class); $decorated->setPublic(true); $decorated->setDecoratedService(Service::class); - $container->setDefinition(Decorated::class, $decorated); + $container->setDefinition(DecoratedService::class, $decorated); $container->compile(); /** @var ServiceLocator $locator */ $locator = $container->get(Locator::class)->locator; static::assertTrue($locator->has(Service::class)); - static::assertFalse($locator->has(Decorated::class)); - static::assertInstanceOf(Decorated::class, $locator->get(Service::class)); + static::assertFalse($locator->has(DecoratedService::class)); + static::assertInstanceOf(DecoratedService::class, $locator->get(Service::class)); } public function testDefinitionOrderIsTheSame() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Config/ContainerParametersResourceCheckerTest.php b/src/Symfony/Component/DependencyInjection/Tests/Config/ContainerParametersResourceCheckerTest.php index 08ff5c797e185..3563a313814db 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Config/ContainerParametersResourceCheckerTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Config/ContainerParametersResourceCheckerTest.php @@ -51,7 +51,7 @@ public function testIsFresh(callable $mockContainer, $expected) $this->assertSame($expected, $this->resourceChecker->isFresh($this->resource, time())); } - public function isFreshProvider() + public static function isFreshProvider() { yield 'not fresh on missing parameter' => [function (MockObject $container) { $container->method('hasParameter')->with('locales')->willReturn(false); @@ -62,11 +62,11 @@ public function isFreshProvider() }, false]; yield 'fresh on every identical parameters' => [function (MockObject $container) { - $container->expects($this->exactly(2))->method('hasParameter')->willReturn(true); - $container->expects($this->exactly(2))->method('getParameter') + $container->expects(self::exactly(2))->method('hasParameter')->willReturn(true); + $container->expects(self::exactly(2))->method('getParameter') ->withConsecutive( - [$this->equalTo('locales')], - [$this->equalTo('default_locale')] + [self::equalTo('locales')], + [self::equalTo('default_locale')] ) ->willReturnMap([ ['locales', ['fr', 'en']], diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index f053ee725930c..8da51e30a616c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -1079,10 +1079,19 @@ public function testPrivateServiceUser() $container->addDefinitions([ 'bar' => $fooDefinition, 'bar_user' => $fooUserDefinition->setPublic(true), + 'bar_user2' => $fooUserDefinition->setPublic(true), ]); $container->compile(); + $this->assertNull($container->get('bar', $container::NULL_ON_INVALID_REFERENCE)); $this->assertInstanceOf(\BarClass::class, $container->get('bar_user')->bar); + + // Ensure that accessing a public service with a shared private service + // does not make the private service available. + $this->assertNull($container->get('bar', $container::NULL_ON_INVALID_REFERENCE)); + + // Ensure the private service is still shared. + $this->assertSame($container->get('bar_user')->bar, $container->get('bar_user2')->bar); } public function testThrowsExceptionWhenSetServiceOnACompiledContainer() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php index 651ab180577ea..5a5b5e98b3771 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php @@ -489,7 +489,13 @@ protected function getBar6Service() */ protected function getDoctrine_ListenerService() { - return $this->privates['doctrine.listener'] = new \stdClass(($this->services['doctrine.entity_manager'] ?? $this->getDoctrine_EntityManagerService())); + $a = ($this->services['doctrine.entity_manager'] ?? $this->getDoctrine_EntityManagerService()); + + if (isset($this->privates['doctrine.listener'])) { + return $this->privates['doctrine.listener']; + } + + return $this->privates['doctrine.listener'] = new \stdClass($a); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php index 57df70800e11a..bf1cd8b1dd661 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php @@ -283,11 +283,16 @@ protected function getDoctrine_EntityListenerResolverService() */ protected function getDoctrine_EntityManagerService() { - $a = new \stdClass(); - $a->resolver = ($this->services['doctrine.entity_listener_resolver'] ?? $this->getDoctrine_EntityListenerResolverService()); - $a->flag = 'ok'; + $a = ($this->services['doctrine.entity_listener_resolver'] ?? $this->getDoctrine_EntityListenerResolverService()); + + if (isset($this->services['doctrine.entity_manager'])) { + return $this->services['doctrine.entity_manager']; + } + $b = new \stdClass(); + $b->resolver = $a; + $b->flag = 'ok'; - return $this->services['doctrine.entity_manager'] = \FactoryChecker::create($a); + return $this->services['doctrine.entity_manager'] = \FactoryChecker::create($b); } /** @@ -297,7 +302,13 @@ protected function getDoctrine_EntityManagerService() */ protected function getDoctrine_ListenerService() { - return $this->services['doctrine.listener'] = new \stdClass(($this->services['doctrine.entity_manager'] ?? $this->getDoctrine_EntityManagerService())); + $a = ($this->services['doctrine.entity_manager'] ?? $this->getDoctrine_EntityManagerService()); + + if (isset($this->services['doctrine.listener'])) { + return $this->services['doctrine.listener']; + } + + return $this->services['doctrine.listener'] = new \stdClass($a); } /** @@ -493,7 +504,13 @@ protected function getLoggerService() */ protected function getMailer_TransportService() { - return $this->services['mailer.transport'] = ($this->services['mailer.transport_factory'] ?? $this->getMailer_TransportFactoryService())->create(); + $a = ($this->services['mailer.transport_factory'] ?? $this->getMailer_TransportFactoryService()); + + if (isset($this->services['mailer.transport'])) { + return $this->services['mailer.transport']; + } + + return $this->services['mailer.transport'] = $a->create(); } /** @@ -516,7 +533,13 @@ protected function getMailer_TransportFactoryService() */ protected function getMailer_TransportFactory_AmazonService() { - return $this->services['mailer.transport_factory.amazon'] = new \stdClass(($this->services['monolog.logger_2'] ?? $this->getMonolog_Logger2Service())); + $a = ($this->services['monolog.logger_2'] ?? $this->getMonolog_Logger2Service()); + + if (isset($this->services['mailer.transport_factory.amazon'])) { + return $this->services['mailer.transport_factory.amazon']; + } + + return $this->services['mailer.transport_factory.amazon'] = new \stdClass($a); } /** diff --git a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php index 737d682207628..0fbd9196ff556 100644 --- a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php @@ -19,7 +19,7 @@ abstract class AbstractCrawlerTest extends TestCase { - abstract public function getDoctype(): string; + abstract public static function getDoctype(): string; protected function createCrawler($node = null, string $uri = null, string $baseHref = null) { diff --git a/src/Symfony/Component/DomCrawler/Tests/Html5ParserCrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/Html5ParserCrawlerTest.php index 374c3ca9493df..7990f5bb48ff5 100644 --- a/src/Symfony/Component/DomCrawler/Tests/Html5ParserCrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/Html5ParserCrawlerTest.php @@ -13,7 +13,7 @@ class Html5ParserCrawlerTest extends AbstractCrawlerTest { - public function getDoctype(): string + public static function getDoctype(): string { return ''; } diff --git a/src/Symfony/Component/DomCrawler/Tests/NativeParserCrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/NativeParserCrawlerTest.php index a17562f735580..c0cac9e8b603f 100644 --- a/src/Symfony/Component/DomCrawler/Tests/NativeParserCrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/NativeParserCrawlerTest.php @@ -13,7 +13,7 @@ class NativeParserCrawlerTest extends AbstractCrawlerTest { - public function getDoctype(): string + public static function getDoctype(): string { return ''; } diff --git a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php index ff06906901cf8..b02a79ecb3777 100644 --- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php +++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php @@ -56,7 +56,7 @@ class DebugClassLoader 'null' => 'null', 'resource' => 'resource', 'boolean' => 'bool', - 'true' => 'bool', + 'true' => 'true', 'false' => 'false', 'integer' => 'int', 'array' => 'array', @@ -74,6 +74,7 @@ class DebugClassLoader '$this' => 'static', 'list' => 'array', 'class-string' => 'string', + 'never' => 'never', ]; private const BUILTIN_RETURN_TYPES = [ @@ -91,6 +92,9 @@ class DebugClassLoader 'parent' => true, 'mixed' => true, 'static' => true, + 'null' => true, + 'true' => true, + 'never' => true, ]; private const MAGIC_METHODS = [ @@ -794,6 +798,12 @@ private function setReturnType(string $types, string $class, string $method, str return; } + if ('null' === $types) { + self::$returnTypes[$class][$method] = ['null', 'null', $class, $filename]; + + return; + } + if ($nullable = str_starts_with($types, 'null|')) { $types = substr($types, 5); } elseif ($nullable = str_ends_with($types, '|null')) { diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php index b3603b261ec79..72a666b867917 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php @@ -50,7 +50,7 @@ public function __construct(bool|callable $debug = false, string $charset = null { $this->debug = \is_bool($debug) ? $debug : $debug(...); $this->charset = $charset ?: (\ini_get('default_charset') ?: 'UTF-8'); - $fileLinkFormat ??= $_SERVER['SYMFONY_IDE'] ?? null; + $fileLinkFormat ??= $_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? null; $this->fileLinkFormat = \is_string($fileLinkFormat) ? (ErrorRendererInterface::IDE_LINK_FORMATS[$fileLinkFormat] ?? $fileLinkFormat ?: false) : ($fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: false); diff --git a/src/Symfony/Component/ErrorHandler/Internal/TentativeTypes.php b/src/Symfony/Component/ErrorHandler/Internal/TentativeTypes.php index 2168a1c075816..1e8afe39b9a4e 100644 --- a/src/Symfony/Component/ErrorHandler/Internal/TentativeTypes.php +++ b/src/Symfony/Component/ErrorHandler/Internal/TentativeTypes.php @@ -753,6 +753,7 @@ class TentativeTypes 'isVariadic' => 'bool', 'isStatic' => 'bool', 'getClosureThis' => '?object', + 'getClosureCalledClass' => '?ReflectionClass', 'getClosureScopeClass' => '?ReflectionClass', 'getDocComment' => 'string|false', 'getEndLine' => 'int|false', diff --git a/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php index fbfcdf90bd8f1..ab2ea41a08c6c 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php @@ -397,6 +397,10 @@ class_exists('Test\\'.ReturnType::class, true); 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::mixed()" might add "mixed" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::nullableMixed()" might add "mixed" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::static()" might add "static" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', + 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::false()" might add "false" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', + 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::true()" might add "true" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', + 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::never()" might add "never" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', + 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::null()" might add "null" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', ], $deprecations); } diff --git a/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ReturnType.php b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ReturnType.php index 21c4d2012f663..1b8138001de78 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ReturnType.php +++ b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ReturnType.php @@ -46,5 +46,9 @@ public function this() { } public function mixed() { } public function nullableMixed() { } public function static() { } + public function false() { } + public function true() { } + public function never() { } + public function null() { } public function outsideMethod() { } } diff --git a/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ReturnTypeParent.php b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ReturnTypeParent.php index c7b0b6f0fca0c..d42c7c8c02167 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ReturnTypeParent.php +++ b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ReturnTypeParent.php @@ -220,6 +220,34 @@ public function static() { } + /** + * @return false + */ + public function false() + { + } + + /** + * @return true + */ + public function true() + { + } + + /** + * @return never + */ + public function never() + { + } + + /** + * @return null + */ + public function null() + { + } + /** * @return int */ diff --git a/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php b/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php index 88329e2ae5309..1c37c6763f4ac 100644 --- a/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php +++ b/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php @@ -50,7 +50,7 @@ public function __construct(callable|array $listener, ?string $name, Stopwatch $ $r = new \ReflectionFunction($listener); if (str_contains($r->name, '{closure}')) { $this->pretty = $this->name = 'closure'; - } elseif ($class = $r->getClosureScopeClass()) { + } elseif ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { $this->name = $class->name; $this->pretty = $this->name.'::'.$r->name; } else { diff --git a/src/Symfony/Component/Finder/Tests/GitignoreTest.php b/src/Symfony/Component/Finder/Tests/GitignoreTest.php index 63f3b76cd5f75..65b52057937b9 100644 --- a/src/Symfony/Component/Finder/Tests/GitignoreTest.php +++ b/src/Symfony/Component/Finder/Tests/GitignoreTest.php @@ -55,7 +55,7 @@ public function testToRegex(array $gitignoreLines, array $matchingCases, array $ } } - public function provider(): array + public static function provider(): array { $cases = [ [ @@ -394,7 +394,7 @@ public function provider(): array public function providerExtended(): array { - $basicCases = $this->provider(); + $basicCases = self::provider(); $cases = []; foreach ($basicCases as $case) { diff --git a/src/Symfony/Component/Finder/Tests/Iterator/SizeRangeFilterIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/SizeRangeFilterIteratorTest.php index 129d565d55b8c..25a6b8a2d75a2 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/SizeRangeFilterIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/SizeRangeFilterIteratorTest.php @@ -41,7 +41,7 @@ public function getAcceptData() ]; return [ - [[new NumberComparator('< 1K'), new NumberComparator('> 0.5K')], $this->toAbsolute($lessThan1KGreaterThan05K)], + [[new NumberComparator('< 1K'), new NumberComparator('> 0.5K')], self::toAbsolute($lessThan1KGreaterThan05K)], ]; } } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php b/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php index 4ac58bd2acd05..55efb652daec7 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php @@ -13,6 +13,7 @@ use Symfony\Component\Form\AbstractRendererEngine; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; @@ -70,8 +71,16 @@ public function buildView(FormView $view, FormInterface $form, array $options) if (!$labelFormat) { $labelFormat = $view->parent->vars['label_format']; } + + $rootFormAttrOption = $form->getRoot()->getConfig()->getOption('form_attr'); + if ($options['form_attr'] || $rootFormAttrOption) { + $options['attr']['form'] = \is_string($rootFormAttrOption) ? $rootFormAttrOption : $form->getRoot()->getName(); + if (empty($options['attr']['form'])) { + throw new LogicException('"form_attr" option must be a string identifier on root form when it has no id.'); + } + } } else { - $id = $name; + $id = \is_string($options['form_attr']) ? $options['form_attr'] : $name; $fullName = $name; $uniqueBlockPrefix = '_'.$blockName; @@ -137,6 +146,7 @@ public function configureOptions(OptionsResolver $resolver) 'translation_domain' => null, 'auto_initialize' => true, 'priority' => 0, + 'form_attr' => false, ]); $resolver->setAllowedTypes('block_prefix', ['null', 'string']); @@ -144,6 +154,7 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('row_attr', 'array'); $resolver->setAllowedTypes('label_html', 'bool'); $resolver->setAllowedTypes('priority', 'int'); + $resolver->setAllowedTypes('form_attr', ['bool', 'string']); $resolver->setInfo('priority', 'The form rendering priority (higher priorities will be rendered first)'); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index b2ad2062363f9..a14f9b13f7f03 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -90,16 +90,6 @@ public function buildView(FormView $view, FormInterface $form, array $options) } $helpTranslationParameters = array_merge($view->parent->vars['help_translation_parameters'], $helpTranslationParameters); - - $rootFormAttrOption = $form->getRoot()->getConfig()->getOption('form_attr'); - if ($options['form_attr'] || $rootFormAttrOption) { - $view->vars['attr']['form'] = \is_string($rootFormAttrOption) ? $rootFormAttrOption : $form->getRoot()->getName(); - if (empty($view->vars['attr']['form'])) { - throw new LogicException('"form_attr" option must be a string identifier on root form when it has no id.'); - } - } - } elseif (\is_string($options['form_attr'])) { - $view->vars['id'] = $options['form_attr']; } $formConfig = $form->getConfig(); @@ -214,7 +204,6 @@ public function configureOptions(OptionsResolver $resolver) 'is_empty_callback' => null, 'getter' => null, 'setter' => null, - 'form_attr' => false, ]); $resolver->setAllowedTypes('label_attr', 'array'); @@ -226,7 +215,6 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('is_empty_callback', ['null', 'callable']); $resolver->setAllowedTypes('getter', ['null', 'callable']); $resolver->setAllowedTypes('setter', ['null', 'callable']); - $resolver->setAllowedTypes('form_attr', ['bool', 'string']); $resolver->setInfo('getter', 'A callable that accepts two arguments (the view data and the current form field) and must return a value.'); $resolver->setInfo('setter', 'A callable that accepts three arguments (a reference to the view data, the submitted value and the current form field).'); diff --git a/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php b/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php index 61b8dc379148a..1d33451d45293 100644 --- a/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php @@ -65,7 +65,7 @@ public function getNormalizedIniPostMaxSize(): string $this->request = null; } - public function methodExceptGetProvider() + public static function methodExceptGetProvider() { return [ ['POST'], @@ -79,7 +79,7 @@ public function methodProvider() { return array_merge([ ['GET'], - ], $this->methodExceptGetProvider()); + ], self::methodExceptGetProvider()); } /** diff --git a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php index 6a4c8552befed..bb2d75b2f3974 100644 --- a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php @@ -206,7 +206,7 @@ public function provideCompletionSuggestions(): iterable yield 'form_type' => [ [''], - $this->getCoreTypes(), + self::getCoreTypes(), ]; yield 'option for FQCN' => [ @@ -225,6 +225,7 @@ public function provideCompletionSuggestions(): iterable 'translation_domain', 'auto_initialize', 'priority', + 'form_attr', ], ]; @@ -244,6 +245,7 @@ public function provideCompletionSuggestions(): iterable 'translation_domain', 'auto_initialize', 'priority', + 'form_attr', ], ]; @@ -258,7 +260,7 @@ public function provideCompletionSuggestions(): iterable ]; } - private function getCoreTypes(): array + private static function getCoreTypes(): array { $coreExtension = new CoreExtension(); $loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes'); diff --git a/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php b/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php index af94dd7e32487..6c73abe370d7e 100644 --- a/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php +++ b/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php @@ -285,15 +285,15 @@ public function privateTaggedServicesProvider() function (ContainerBuilder $container) { $formTypes = $container->getDefinition('form.extension')->getArgument(0); - $this->assertInstanceOf(Reference::class, $formTypes); + self::assertInstanceOf(Reference::class, $formTypes); $locator = $container->getDefinition((string) $formTypes); $expectedLocatorMap = [ 'stdClass' => new ServiceClosureArgument(new Reference('my.type')), ]; - $this->assertInstanceOf(Definition::class, $locator); - $this->assertEquals($expectedLocatorMap, $locator->getArgument(0)); + self::assertInstanceOf(Definition::class, $locator); + self::assertEquals($expectedLocatorMap, $locator->getArgument(0)); }, ], [ @@ -301,7 +301,7 @@ function (ContainerBuilder $container) { Type1TypeExtension::class, 'form.type_extension', function (ContainerBuilder $container) { - $this->assertEquals( + self::assertEquals( ['Symfony\Component\Form\Extension\Core\Type\FormType' => new IteratorArgument([new Reference('my.type_extension')])], $container->getDefinition('form.extension')->getArgument(1) ); @@ -309,7 +309,7 @@ function (ContainerBuilder $container) { ['extended_type' => 'Symfony\Component\Form\Extension\Core\Type\FormType'], ], ['my.guesser', 'stdClass', 'form.type_guesser', function (ContainerBuilder $container) { - $this->assertEquals(new IteratorArgument([new Reference('my.guesser')]), $container->getDefinition('form.extension')->getArgument(2)); + self::assertEquals(new IteratorArgument([new Reference('my.guesser')]), $container->getDefinition('form.extension')->getArgument(2)); }], ]; } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToRfc3339TransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToRfc3339TransformerTest.php index eccaa22a136f3..0412dc321a0f6 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToRfc3339TransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToRfc3339TransformerTest.php @@ -37,7 +37,7 @@ protected function tearDown(): void $this->dateTimeWithoutSeconds = null; } - public function allProvider() + public static function allProvider() { return [ ['UTC', 'UTC', '2010-02-03 04:05:06 UTC', '2010-02-03T04:05:06Z'], @@ -51,12 +51,12 @@ public function allProvider() public function transformProvider() { - return $this->allProvider(); + return self::allProvider(); } public function reverseTransformProvider() { - return array_merge($this->allProvider(), [ + return array_merge(self::allProvider(), [ // format without seconds, as appears in some browsers ['UTC', 'UTC', '2010-02-03 04:05:00 UTC', '2010-02-03T04:05Z'], ['America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:00 America/New_York', '2010-02-03T17:05+08:00'], diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ButtonTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ButtonTypeTest.php index 654e04649e9f1..dcff028026482 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ButtonTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ButtonTypeTest.php @@ -13,6 +13,8 @@ use Symfony\Component\Form\Button; use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\Type\FormType; /** * @author Bernhard Schussek @@ -36,4 +38,65 @@ public function testSubmitNullUsesDefaultEmptyData($emptyData = 'empty', $expect $this->expectExceptionMessage('Buttons do not support empty data.'); parent::testSubmitNullUsesDefaultEmptyData($emptyData, $expectedData); } + + public function testFormAttrOnRoot() + { + $view = $this->factory + ->createNamedBuilder('parent', FormType::class, null, [ + 'form_attr' => true, + ]) + ->add('child1', $this->getTestedType()) + ->add('child2', $this->getTestedType()) + ->getForm() + ->createView(); + $this->assertArrayNotHasKey('form', $view->vars['attr']); + $this->assertSame($view->vars['id'], $view['child1']->vars['attr']['form']); + $this->assertSame($view->vars['id'], $view['child2']->vars['attr']['form']); + } + + public function testFormAttrOnChild() + { + $view = $this->factory + ->createNamedBuilder('parent') + ->add('child1', $this->getTestedType(), [ + 'form_attr' => true, + ]) + ->add('child2', $this->getTestedType()) + ->getForm() + ->createView(); + $this->assertArrayNotHasKey('form', $view->vars['attr']); + $this->assertSame($view->vars['id'], $view['child1']->vars['attr']['form']); + $this->assertArrayNotHasKey('form', $view['child2']->vars['attr']); + } + + public function testFormAttrAsBoolWithNoId() + { + $this->expectException(LogicException::class); + $this->expectErrorMessage('form_attr'); + $this->factory + ->createNamedBuilder('', FormType::class, null, [ + 'form_attr' => true, + ]) + ->add('child1', $this->getTestedType()) + ->add('child2', $this->getTestedType()) + ->getForm() + ->createView(); + } + + public function testFormAttrAsStringWithNoId() + { + $stringId = 'custom-identifier'; + $view = $this->factory + ->createNamedBuilder('', FormType::class, null, [ + 'form_attr' => $stringId, + ]) + ->add('child1', $this->getTestedType()) + ->add('child2', $this->getTestedType()) + ->getForm() + ->createView(); + $this->assertArrayNotHasKey('form', $view->vars['attr']); + $this->assertSame($stringId, $view->vars['id']); + $this->assertSame($view->vars['id'], $view['child1']->vars['attr']['form']); + $this->assertSame($view->vars['id'], $view['child2']->vars['attr']['form']); + } } diff --git a/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php b/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php index 3216244e9ed10..18fec32dee43d 100644 --- a/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php +++ b/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php @@ -45,6 +45,33 @@ public function provideSanitize() 'output' => null, ]; + yield [ + 'input' => 'http://trusted.com/link.php', + 'allowedSchemes' => null, + 'allowedHosts' => null, + 'forceHttps' => false, + 'allowRelative' => false, + 'output' => 'http://trusted.com/link.php', + ]; + + yield [ + 'input' => 'https://trusted.com/link.php', + 'allowedSchemes' => null, + 'allowedHosts' => null, + 'forceHttps' => false, + 'allowRelative' => false, + 'output' => 'https://trusted.com/link.php', + ]; + + yield [ + 'input' => 'data:text/plain;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', + 'allowedSchemes' => null, + 'allowedHosts' => null, + 'forceHttps' => false, + 'allowRelative' => false, + 'output' => 'data:text/plain;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', + ]; + yield [ 'input' => 'https://trusted.com/link.php', 'allowedSchemes' => ['https'], diff --git a/src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php b/src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php index b127c1ba60a0c..53645a12f4578 100644 --- a/src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php +++ b/src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php @@ -32,7 +32,7 @@ public function process(ContainerBuilder $container) $container->register('.debug.'.$id, TraceableHttpClient::class) ->setArguments([new Reference('.debug.'.$id.'.inner'), new Reference('debug.stopwatch', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)]) ->addTag('kernel.reset', ['method' => 'reset']) - ->setDecoratedService($id); + ->setDecoratedService($id, null, 5); $container->getDefinition('data_collector.http_client') ->addMethodCall('registerClient', [$id, new Reference('.debug.'.$id)]); } diff --git a/src/Symfony/Component/HttpClient/Tests/DependencyInjection/HttpClientPassTest.php b/src/Symfony/Component/HttpClient/Tests/DependencyInjection/HttpClientPassTest.php index eb04f88226d1f..c6dcf4fcf7902 100755 --- a/src/Symfony/Component/HttpClient/Tests/DependencyInjection/HttpClientPassTest.php +++ b/src/Symfony/Component/HttpClient/Tests/DependencyInjection/HttpClientPassTest.php @@ -38,7 +38,7 @@ public function testItDecoratesHttpClientWithTraceableHttpClient() $sut->process($container); $this->assertTrue($container->hasDefinition('.debug.foo')); $this->assertSame(TraceableHttpClient::class, $container->getDefinition('.debug.foo')->getClass()); - $this->assertSame(['foo', null, 0], $container->getDefinition('.debug.foo')->getDecoratedService()); + $this->assertSame(['foo', null, 5], $container->getDefinition('.debug.foo')->getDecoratedService()); } public function testItRegistersDebugHttpClientToCollector() diff --git a/src/Symfony/Component/HttpFoundation/Exception/SessionNotFoundException.php b/src/Symfony/Component/HttpFoundation/Exception/SessionNotFoundException.php index 9c719aa041be3..94b0cb69aae1f 100644 --- a/src/Symfony/Component/HttpFoundation/Exception/SessionNotFoundException.php +++ b/src/Symfony/Component/HttpFoundation/Exception/SessionNotFoundException.php @@ -12,7 +12,7 @@ namespace Symfony\Component\HttpFoundation\Exception; /** - * Raised when a session does not exists. This happens in the following cases: + * Raised when a session does not exist. This happens in the following cases: * - the session is not enabled * - attempt to read a session outside a request context (ie. cli script). * diff --git a/src/Symfony/Component/HttpFoundation/IpUtils.php b/src/Symfony/Component/HttpFoundation/IpUtils.php index 49f433b76cc30..746720ea78ea2 100644 --- a/src/Symfony/Component/HttpFoundation/IpUtils.php +++ b/src/Symfony/Component/HttpFoundation/IpUtils.php @@ -114,10 +114,6 @@ public static function checkIp6(string $requestIp, string $ip): bool } // Check to see if we were given a IP4 $requestIp or $ip by mistake - if (str_contains($requestIp, '.') || str_contains($ip, '.')) { - return self::$checkedIps[$cacheKey] = false; - } - if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { return self::$checkedIps[$cacheKey] = false; } @@ -125,6 +121,10 @@ public static function checkIp6(string $requestIp, string $ip): bool if (str_contains($ip, '/')) { [$address, $netmask] = explode('/', $ip, 2); + if (!filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + return self::$checkedIps[$cacheKey] = false; + } + if ('0' === $netmask) { return (bool) unpack('n*', @inet_pton($address)); } @@ -133,6 +133,10 @@ public static function checkIp6(string $requestIp, string $ip): bool return self::$checkedIps[$cacheKey] = false; } } else { + if (!filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + return self::$checkedIps[$cacheKey] = false; + } + $address = $ip; $netmask = 128; } diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index f430bfd8869b8..b39b9c062be12 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -502,10 +502,10 @@ public function __toString(): string $cookies = []; foreach ($this->cookies as $k => $v) { - $cookies[] = $k.'='.$v; + $cookies[] = \is_array($v) ? http_build_query([$k => $v], '', '; ', \PHP_QUERY_RFC3986) : "$k=$v"; } - if (!empty($cookies)) { + if ($cookies) { $cookieHeader = 'Cookie: '.implode('; ', $cookies)."\r\n"; } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php index 3c908e7771f31..04c1afdd38dc0 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php @@ -69,8 +69,7 @@ protected function doRead(string $sessionId): string public function updateTimestamp(string $sessionId, string $data): bool { - $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); - $this->memcached->touch($this->prefix.$sessionId, time() + (int) $ttl); + $this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl()); return true; } @@ -79,10 +78,21 @@ public function updateTimestamp(string $sessionId, string $data): bool * {@inheritdoc} */ protected function doWrite(string $sessionId, string $data): bool + { + return $this->memcached->set($this->prefix.$sessionId, $data, $this->getCompatibleTtl()); + } + + private function getCompatibleTtl(): int { $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); - return $this->memcached->set($this->prefix.$sessionId, $data, time() + (int) $ttl); + // If the relative TTL that is used exceeds 30 days, memcached will treat the value as Unix time. + // We have to convert it to an absolute Unix time at this point, to make sure the TTL is correct. + if ($ttl > 60 * 60 * 24 * 30) { + $ttl += time(); + } + + return $ttl; } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/IpUtilsTest.php b/src/Symfony/Component/HttpFoundation/Tests/IpUtilsTest.php index 9db54719a65b5..6085331b28bf3 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/IpUtilsTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/IpUtilsTest.php @@ -78,6 +78,7 @@ public function getIpv6Data() [false, '0.0.0.0/8', '::1'], [false, '::1', '127.0.0.1'], [false, '::1', '0.0.0.0/8'], + [true, '::ffff:10.126.42.2', '::ffff:10.0.0.0/0'], ]; } diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index 65e5cdcad3798..06b8e4063e33e 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -1699,6 +1699,12 @@ public function testToString() $asString = (string) $request; $this->assertStringContainsString('Cookie: Foo=Bar; Another=Cookie', $asString); + + $request->cookies->set('foo.bar', [1, 2]); + + $asString = (string) $request; + + $this->assertStringContainsString('foo.bar%5B0%5D=1; foo.bar%5B1%5D=2', $asString); } public function testIsMethod() diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index eac380fd9094a..3e0e7844104b4 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -16,6 +16,7 @@ /** * @requires extension memcached + * * @group time-sensitive */ class MemcachedSessionHandlerTest extends TestCase @@ -92,13 +93,30 @@ public function testWriteSession() $this->memcached ->expects($this->once()) ->method('set') - ->with(self::PREFIX.'id', 'data', $this->equalTo(time() + self::TTL, 2)) + ->with(self::PREFIX.'id', 'data', $this->equalTo(self::TTL, 2)) ->willReturn(true) ; $this->assertTrue($this->storage->write('id', 'data')); } + public function testWriteSessionWithLargeTTL() + { + $this->memcached + ->expects($this->once()) + ->method('set') + ->with(self::PREFIX.'id', 'data', $this->equalTo(time() + self::TTL + 60 * 60 * 24 * 30, 2)) + ->willReturn(true) + ; + + $storage = new MemcachedSessionHandler( + $this->memcached, + ['prefix' => self::PREFIX, 'expiretime' => self::TTL + 60 * 60 * 24 * 30] + ); + + $this->assertTrue($storage->write('id', 'data')); + } + public function testDestroySession() { $this->memcached diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php index e9a7494a4c761..f014b5bcb82df 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php @@ -39,9 +39,10 @@ public function supports(Request $request, ArgumentMetadata $argument): bool public function resolve(Request $request, ArgumentMetadata $argument): iterable { $value = $request->attributes->get($argument->getName()); + $class = \DateTimeInterface::class === $argument->getType() ? \DateTimeImmutable::class : $argument->getType(); if ($value instanceof \DateTimeInterface) { - yield $value; + yield $value instanceof $class ? $value : $class::createFromInterface($value); return; } @@ -52,7 +53,6 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable return; } - $class = \DateTimeInterface::class === $argument->getType() ? \DateTimeImmutable::class : $argument->getType(); $format = null; if ($attributes = $argument->getAttributes(MapDateTime::class, ArgumentMetadata::IS_INSTANCEOF)) { diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php index 921fba03c204f..890589d652072 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php @@ -33,7 +33,7 @@ public function createArgumentMetadata(string|object|array $controller): array $class = $reflection->class; } else { $reflection = new \ReflectionFunction($controller); - if ($class = str_contains($reflection->name, '{closure}') ? null : $reflection->getClosureScopeClass()) { + if ($class = str_contains($reflection->name, '{closure}') ? null : (\PHP_VERSION_ID >= 80111 ? $reflection->getClosureCalledClass() : $reflection->getClosureScopeClass())) { $class = $class->name; } } diff --git a/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php index ca1764cb46a49..8f45610b2c6ba 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php @@ -477,7 +477,7 @@ private function parseController(array|object|string|null $controller): array|st } $controller['method'] = $r->name; - if ($class = $r->getClosureScopeClass()) { + if ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { $controller['class'] = $class->name; } else { return $r->name; diff --git a/src/Symfony/Component/HttpKernel/Debug/FileLinkFormatter.php b/src/Symfony/Component/HttpKernel/Debug/FileLinkFormatter.php index 094465274181b..4eaeeb1513713 100644 --- a/src/Symfony/Component/HttpKernel/Debug/FileLinkFormatter.php +++ b/src/Symfony/Component/HttpKernel/Debug/FileLinkFormatter.php @@ -35,7 +35,8 @@ class FileLinkFormatter */ public function __construct(string|array $fileLinkFormat = null, RequestStack $requestStack = null, string $baseDir = null, string|\Closure $urlFormat = null) { - $fileLinkFormat ??= $_SERVER['SYMFONY_IDE'] ?? ''; + $fileLinkFormat ??= $_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? ''; + if (!\is_array($fileLinkFormat) && $fileLinkFormat = (ErrorRendererInterface::IDE_LINK_FORMATS[$fileLinkFormat] ?? $fileLinkFormat) ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: false) { $i = strpos($f = $fileLinkFormat, '&', max(strrpos($f, '%f'), strrpos($f, '%l'))) ?: \strlen($f); $fileLinkFormat = [substr($f, 0, $i)] + preg_split('/&([^>]++)>/', substr($f, $i), -1, \PREG_SPLIT_DELIM_CAPTURE); diff --git a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php index 05be006e1722a..e57bb17e6faf4 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php @@ -195,10 +195,11 @@ public function onKernelResponse(ResponseEvent $event) } if ($autoCacheControl) { + $maxAge = $response->headers->hasCacheControlDirective('public') ? 0 : (int) $response->getMaxAge(); $response - ->setExpires(new \DateTime()) + ->setExpires(new \DateTimeImmutable('+'.$maxAge.' seconds')) ->setPrivate() - ->setMaxAge(0) + ->setMaxAge($maxAge) ->headers->addCacheControlDirective('must-revalidate'); } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index f279be75340c0..22efec052530d 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -78,11 +78,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.1.8'; - public const VERSION_ID = 60108; + public const VERSION = '6.1.9'; + public const VERSION_ID = 60109; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 1; - public const RELEASE_VERSION = 8; + public const RELEASE_VERSION = 9; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '01/2023'; diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php index b438c527a32da..ac10dfc1a5ea7 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php @@ -20,7 +20,7 @@ class DateTimeValueResolverTest extends TestCase { - private $defaultTimezone; + private readonly string $defaultTimezone; protected function setUp(): void { @@ -32,13 +32,20 @@ protected function tearDown(): void date_default_timezone_set($this->defaultTimezone); } - public function getTimeZones() + public static function getTimeZones() { yield ['UTC']; yield ['Etc/GMT+9']; yield ['Etc/GMT-14']; } + public static function getClasses() + { + yield [\DateTimeInterface::class]; + yield [\DateTime::class]; + yield [FooDateTime::class]; + } + public function testSupports() { $resolver = new DateTimeValueResolver(); @@ -133,11 +140,16 @@ public function testNow(string $timezone) $this->assertSame($timezone, $results[0]->getTimezone()->getName(), 'Default timezone'); } - public function testPreviouslyConvertedAttribute() + /** + * @param class-string<\DateTimeInterface> $class + * + * @dataProvider getClasses + */ + public function testPreviouslyConvertedAttribute(string $class) { $resolver = new DateTimeValueResolver(); - $argument = new ArgumentMetadata('dummy', \DateTime::class, false, false, null, true); + $argument = new ArgumentMetadata('dummy', $class, false, false, null, true); $request = self::requestWithAttributes(['dummy' => $datetime = new \DateTime()]); /** @var \Generator $results */ @@ -145,7 +157,8 @@ public function testPreviouslyConvertedAttribute() $results = iterator_to_array($results); $this->assertCount(1, $results); - $this->assertSame($datetime, $results[0]); + $this->assertEquals($datetime, $results[0], 'The value is the same, but the class can be modified.'); + $this->assertInstanceOf($class, $results[0]); } public function testCustomClass() @@ -209,7 +222,7 @@ public function testWithFormat(string $timezone) $this->assertEquals('2016-09-08 12:34:56', $results[0]->format('Y-m-d H:i:s')); } - public function provideInvalidDates() + public static function provideInvalidDates() { return [ 'invalid date' => [ diff --git a/src/Symfony/Component/HttpKernel/Tests/Debug/FileLinkFormatterTest.php b/src/Symfony/Component/HttpKernel/Tests/Debug/FileLinkFormatterTest.php index d24958c6c5de5..57605c1ae4ec2 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Debug/FileLinkFormatterTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Debug/FileLinkFormatterTest.php @@ -27,9 +27,17 @@ public function testWhenNoFileLinkFormatAndNoRequest() public function testAfterUnserialize() { + $ide = $_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? null; + $_ENV['SYMFONY_IDE'] = $_SERVER['SYMFONY_IDE'] = null; $sut = unserialize(serialize(new FileLinkFormatter())); $this->assertFalse($sut->format('/kernel/root/src/my/very/best/file.php', 3)); + + if (null === $ide) { + unset($_ENV['SYMFONY_IDE'], $_SERVER['SYMFONY_IDE']); + } else { + $_ENV['SYMFONY_IDE'] = $_SERVER['SYMFONY_IDE'] = $ide; + } } public function testWhenFileLinkFormatAndNoRequest() diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index 302f9e35dd70d..d6be9d3bd023c 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -467,7 +467,7 @@ public function testAutowireAttribute() $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service', 'stdClass')->addArgument([]); - $container->register('some.id', \stdClass::class); + $container->register('some.id', \stdClass::class)->setPublic(true); $container->setParameter('some.parameter', 'foo'); $container->register('foo', WithAutowireAttribute::class) diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php index 02db1946c705c..bc48f4b6cb6ad 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php @@ -38,6 +38,7 @@ class SessionListenerTest extends TestCase { /** * @dataProvider provideSessionOptions + * * @runInSeparateProcess */ public function testSessionCookieOptions(array $phpSessionOptions, array $sessionOptions, array $expectedSessionOptions) @@ -554,6 +555,69 @@ public function testUninitializedSessionWithoutInitializedSession() $this->assertSame('60', $response->headers->getCacheControlDirective('s-maxage')); } + public function testResponseHeadersMaxAgeAndExpiresNotBeOverridenIfSessionStarted() + { + $session = $this->createMock(Session::class); + $session->expects($this->once())->method('getUsageIndex')->willReturn(1); + $session->expects($this->once())->method('getName')->willReturn('foo'); + $sessionFactory = $this->createMock(SessionFactory::class); + $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); + + $container = new Container(); + $container->set('session_factory', $sessionFactory); + + $listener = new SessionListener($container); + $kernel = $this->createMock(HttpKernelInterface::class); + + $request = new Request(); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $request->getSession(); + + $response = new Response(); + $response->setPrivate(); + $expiresHeader = gmdate('D, d M Y H:i:s', time() + 600).' GMT'; + $response->setMaxAge(600); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + + $this->assertTrue($response->headers->has('expires')); + $this->assertSame($expiresHeader, $response->headers->get('expires')); + $this->assertFalse($response->headers->has('max-age')); + $this->assertSame('600', $response->headers->getCacheControlDirective('max-age')); + $this->assertFalse($response->headers->hasCacheControlDirective('public')); + $this->assertTrue($response->headers->hasCacheControlDirective('private')); + $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); + $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); + } + + public function testResponseHeadersMaxAgeAndExpiresDefaultValuesIfSessionStarted() + { + $session = $this->createMock(Session::class); + $session->expects($this->once())->method('getUsageIndex')->willReturn(1); + + $container = new Container(); + + $listener = new SessionListener($container); + $kernel = $this->createMock(HttpKernelInterface::class); + + $request = new Request(); + $request->setSession($session); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $response = new Response(); + $expiresHeader = gmdate('D, d M Y H:i:s', time()).' GMT'; + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + + $this->assertTrue($response->headers->has('expires')); + $this->assertSame($expiresHeader, $response->headers->get('expires')); + $this->assertFalse($response->headers->has('max-age')); + $this->assertSame('0', $response->headers->getCacheControlDirective('max-age')); + $this->assertFalse($response->headers->hasCacheControlDirective('public')); + $this->assertTrue($response->headers->hasCacheControlDirective('private')); + $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); + $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); + } + public function testSurrogateMainRequestIsPublic() { $session = $this->createMock(Session::class); diff --git a/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php index b6828bbaac76d..78d53b994a0c8 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Transport\RoundRobinTransport; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mime\RawMessage; @@ -60,10 +61,21 @@ public function testSendAllDead() $t2 = $this->createMock(TransportInterface::class); $t2->expects($this->once())->method('send')->will($this->throwException(new TransportException())); $t = new RoundRobinTransport([$t1, $t2]); - $this->expectException(TransportException::class); - $this->expectExceptionMessage('All transports failed.'); - $t->send(new RawMessage('')); - $this->assertTransports($t, 1, [$t1, $t2]); + $p = new \ReflectionProperty($t, 'cursor'); + $p->setAccessible(true); + $p->setValue($t, 0); + + try { + $t->send(new RawMessage('')); + } catch (\Exception $e) { + $this->assertInstanceOf(TransportException::class, $e); + $this->assertStringContainsString('All transports failed.', $e->getMessage()); + $this->assertTransports($t, 0, [$t1, $t2]); + + return; + } + + $this->fail('The expected exception was not thrown.'); } public function testSendOneDead() @@ -124,6 +136,34 @@ public function testSendOneDeadAndRecoveryWithinRetryPeriod() $this->assertTransports($t, 1, []); } + public function testFailureDebugInformation() + { + $t1 = $this->createMock(TransportInterface::class); + $e1 = new TransportException(); + $e1->appendDebug('Debug message 1'); + $t1->expects($this->once())->method('send')->will($this->throwException($e1)); + $t1->expects($this->once())->method('__toString')->willReturn('t1'); + + $t2 = $this->createMock(TransportInterface::class); + $e2 = new TransportException(); + $e2->appendDebug('Debug message 2'); + $t2->expects($this->once())->method('send')->will($this->throwException($e2)); + $t2->expects($this->once())->method('__toString')->willReturn('t2'); + + $t = new RoundRobinTransport([$t1, $t2]); + + try { + $t->send(new RawMessage('')); + } catch (TransportExceptionInterface $e) { + $this->assertStringContainsString('Transport "t1": Debug message 1', $e->getDebug()); + $this->assertStringContainsString('Transport "t2": Debug message 2', $e->getDebug()); + + return; + } + + $this->fail('Expected exception was not thrown!'); + } + private function assertTransports(RoundRobinTransport $transport, int $cursor, array $deadTransports) { $p = new \ReflectionProperty($transport, 'cursor'); diff --git a/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php b/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php index ab0e40458b658..c5587bb2a3585 100644 --- a/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php +++ b/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php @@ -48,15 +48,19 @@ public function __construct(array $transports, int $retryPeriod = 60) public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { + $exception = null; + while ($transport = $this->getNextTransport()) { try { return $transport->send($message, $envelope); - } catch (TransportExceptionInterface) { + } catch (TransportExceptionInterface $e) { + $exception ??= new TransportException('All transports failed.'); + $exception->appendDebug(sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug())); $this->deadTransports[$transport] = microtime(true); } } - throw new TransportException('All transports failed.'); + throw $exception ?? new TransportException('No transports found.'); } public function __toString(): string diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php index 81c2f441f6fc6..f6dfe8b6adcbc 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php @@ -32,6 +32,9 @@ class Connection 'x-message-ttl', ]; + /** + * @see https://github.com/php-amqp/php-amqp/blob/master/amqp_connection_resource.h + */ private const AVAILABLE_OPTIONS = [ 'host', 'port', @@ -52,6 +55,7 @@ class Connection 'write_timeout', 'confirm_timeout', 'connect_timeout', + 'rpc_timeout', 'cacert', 'cert', 'key', diff --git a/src/Symfony/Component/Messenger/Handler/HandlerDescriptor.php b/src/Symfony/Component/Messenger/Handler/HandlerDescriptor.php index 8b872bd94b9cb..9fcd558e69924 100644 --- a/src/Symfony/Component/Messenger/Handler/HandlerDescriptor.php +++ b/src/Symfony/Component/Messenger/Handler/HandlerDescriptor.php @@ -35,7 +35,7 @@ public function __construct(callable $handler, array $options = []) if (str_contains($r->name, '{closure}')) { $this->name = 'Closure'; } elseif (!$handler = $r->getClosureThis()) { - $class = $r->getClosureScopeClass(); + $class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass(); $this->name = ($class ? $class->name.'::' : '').$r->name; } else { diff --git a/src/Symfony/Component/Mime/Part/MessagePart.php b/src/Symfony/Component/Mime/Part/MessagePart.php index 1b5c23e2bc411..270d57aa343ac 100644 --- a/src/Symfony/Component/Mime/Part/MessagePart.php +++ b/src/Symfony/Component/Mime/Part/MessagePart.php @@ -59,4 +59,14 @@ public function bodyToIterable(): iterable { return $this->message->toIterable(); } + + public function __sleep(): array + { + return ['message']; + } + + public function __wakeup() + { + $this->__construct($this->message); + } } diff --git a/src/Symfony/Component/Mime/Tests/Part/MessagePartTest.php b/src/Symfony/Component/Mime/Tests/Part/MessagePartTest.php index 2713d5bc079c7..c01958a4b94b8 100644 --- a/src/Symfony/Component/Mime/Tests/Part/MessagePartTest.php +++ b/src/Symfony/Component/Mime/Tests/Part/MessagePartTest.php @@ -39,4 +39,14 @@ public function testHeaders() new ParameterizedHeader('Content-Disposition', 'attachment', ['name' => 'Subject.eml', 'filename' => 'Subject.eml']) ), $p->getPreparedHeaders()); } + + public function testSerialize() + { + $email = (new Email())->from('fabien@symfony.com')->to('you@example.com')->text('content'); + $email->getHeaders()->addIdHeader('Message-ID', $email->generateMessageId()); + + $p = new MessagePart($email); + $expected = clone $p; + $this->assertEquals($expected->toString(), unserialize(serialize($p))->toString()); + } } diff --git a/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md b/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md index 3c32a8eef1d00..3dbcbb1546247 100644 --- a/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md +++ b/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md @@ -13,7 +13,7 @@ AMAZON_SNS_DSN=sns://ACCESS_ID:ACCESS_KEY@default?region=REGION Resources --------- - * [Contributing](https://symfony.com/doc/current/contributing/index.html) - * [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json b/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json index 9727e1e5cc472..806aafe7c6e5f 100644 --- a/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json @@ -24,9 +24,6 @@ "symfony/http-client": "^5.4|^6.0", "symfony/notifier": "^5.4|^6.0" }, - "require-dev": { - "symfony/event-dispatcher": "^5.4|^6.0" - }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Clickatell\\": "" }, "exclude-from-classmap": [ diff --git a/src/Symfony/Component/Notifier/Bridge/Discord/composer.json b/src/Symfony/Component/Notifier/Bridge/Discord/composer.json index ce9f0c05d7fa9..ba5e4185cf383 100644 --- a/src/Symfony/Component/Notifier/Bridge/Discord/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Discord/composer.json @@ -21,9 +21,6 @@ "symfony/notifier": "^5.4|^6.0", "symfony/polyfill-mbstring": "^1.0" }, - "require-dev": { - "symfony/event-dispatcher": "^5.4|^6.0" - }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Discord\\": "" }, "exclude-from-classmap": [ diff --git a/src/Symfony/Component/Notifier/Bridge/Engagespot/composer.json b/src/Symfony/Component/Notifier/Bridge/Engagespot/composer.json index 0bc0684cb174d..2cffdda7bce52 100644 --- a/src/Symfony/Component/Notifier/Bridge/Engagespot/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Engagespot/composer.json @@ -16,8 +16,8 @@ } ], "require": { - "php": ">=7.2.5", - "symfony/http-client": "^4.3|^5.0|^6.0", + "php": ">=8.1", + "symfony/http-client": "^5.4|^6.0", "symfony/notifier": "^5.4|^6.0" }, "autoload": { diff --git a/src/Symfony/Component/Notifier/Bridge/FakeChat/composer.json b/src/Symfony/Component/Notifier/Bridge/FakeChat/composer.json index e2343957bb4d3..fbe5d368aba72 100644 --- a/src/Symfony/Component/Notifier/Bridge/FakeChat/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/FakeChat/composer.json @@ -24,7 +24,6 @@ "php": ">=8.1", "symfony/http-client": "^5.4|^6.0", "symfony/notifier": "^5.4|^6.0", - "symfony/event-dispatcher-contracts": "^2|^3", "symfony/mailer": "^5.4|^6.0" }, "autoload": { diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/composer.json b/src/Symfony/Component/Notifier/Bridge/FakeSms/composer.json index 29fb61b294923..4145a0a3c2de6 100644 --- a/src/Symfony/Component/Notifier/Bridge/FakeSms/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/composer.json @@ -24,7 +24,6 @@ "php": ">=8.1", "symfony/http-client": "^5.4|^6.0", "symfony/notifier": "^5.4|^6.0", - "symfony/event-dispatcher-contracts": "^2|^3", "symfony/mailer": "^5.4|^6.0" }, "autoload": { diff --git a/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/composer.json b/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/composer.json index 518b2ab3ad434..aa92942582a16 100644 --- a/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/composer.json @@ -1,32 +1,32 @@ { - "name": "symfony/kaz-info-teh-notifier", - "type": "symfony-bridge", - "description": "Symfony KazInfoTeh Notifier Bridge", - "keywords": ["KazInfoTeh", "notifier", "symfony", "sms"], - "homepage": "https://symfony.com", - "license": "MIT", - "authors": [ - { - "name": "Egor Taranov", - "email": "dev@taranovegor.com", - "homepage": "https://taranovegor.com/contribution" + "name": "symfony/kaz-info-teh-notifier", + "type": "symfony-bridge", + "description": "Symfony KazInfoTeh Notifier Bridge", + "keywords": ["KazInfoTeh", "notifier", "symfony", "sms"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Egor Taranov", + "email": "dev@taranovegor.com", + "homepage": "https://taranovegor.com/contribution" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "ext-simplexml": "*", + "symfony/http-client": "^5.4|^6.0", + "symfony/notifier": "^5.4|^6.0" }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "require": { - "php": ">=8.1", - "ext-simplexml": "*", - "symfony/http-client": "^5.4|^6.0", - "symfony/notifier": "^5.4|^6.0" - }, - "autoload": { - "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\KazInfoTeh\\": "" }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "minimum-stability": "dev" + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\KazInfoTeh\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" } diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json b/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json index 90809726175c4..8361ffe879abe 100644 --- a/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json @@ -1,30 +1,30 @@ { - "name": "symfony/message-media-notifier", - "type": "symfony-notifier-bridge", - "description": "Symfony MessageMedia Notifier Bridge", - "keywords": ["sms", "messagemedia", "notifier"], - "homepage": "https://symfony.com", - "license": "MIT", - "authors": [ - { - "name": "Adrian Nguyen", - "email": "vuphuong87@gmail.com" + "name": "symfony/message-media-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony MessageMedia Notifier Bridge", + "keywords": ["sms", "messagemedia", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Adrian Nguyen", + "email": "vuphuong87@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/http-client": "^5.4|^6.0", + "symfony/notifier": "^5.4|^6.0" }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "require": { - "php": ">=8.1", - "symfony/http-client": "^5.4|^6.0", - "symfony/notifier": "^5.4|^6.0" - }, - "autoload": { - "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\MessageMedia\\": "" }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "minimum-stability": "dev" + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\MessageMedia\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" } diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json b/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json index 9e5c68474681a..d06a34b68630e 100644 --- a/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json @@ -20,9 +20,6 @@ "symfony/http-client": "^5.4|^6.0", "symfony/notifier": "^5.4|^6.0" }, - "require-dev": { - "symfony/event-dispatcher": "^5.4|^6.0" - }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\OneSignal\\": "" }, "exclude-from-classmap": [ diff --git a/src/Symfony/Component/Notifier/Bridge/Sendberry/README.md b/src/Symfony/Component/Notifier/Bridge/Sendberry/README.md index 5dc8b646bf485..c4585cb5dd90d 100644 --- a/src/Symfony/Component/Notifier/Bridge/Sendberry/README.md +++ b/src/Symfony/Component/Notifier/Bridge/Sendberry/README.md @@ -19,7 +19,7 @@ where: Resources --------- - * [Contributing](https://symfony.com/doc/current/contributing/index.html) - * [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Notifier/Bridge/Sendberry/composer.json b/src/Symfony/Component/Notifier/Bridge/Sendberry/composer.json index aebeebf79ccb2..eb754ebb7a6e6 100644 --- a/src/Symfony/Component/Notifier/Bridge/Sendberry/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Sendberry/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/http-client": "^5.4|^6.0", "symfony/notifier": "^5.4|^6.0" }, diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/composer.json b/src/Symfony/Component/Notifier/Bridge/Slack/composer.json index 314eafa05b13b..d79dcafa34b90 100644 --- a/src/Symfony/Component/Notifier/Bridge/Slack/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Slack/composer.json @@ -20,9 +20,6 @@ "symfony/http-client": "^5.4|^6.0", "symfony/notifier": "^5.4|^6.0" }, - "require-dev": { - "symfony/event-dispatcher": "^5.4|^6.0" - }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Slack\\": "" }, "exclude-from-classmap": [ diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json b/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json index c82f69ba421c5..3fff5b9f5ace3 100644 --- a/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json @@ -20,9 +20,6 @@ "symfony/http-client": "^5.4|^6.0", "symfony/notifier": "^5.4|^6.0" }, - "require-dev": { - "symfony/event-dispatcher": "^5.4|^6.0" - }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Telegram\\": "" }, "exclude-from-classmap": [ diff --git a/src/Symfony/Component/Notifier/Bridge/Telnyx/README.md b/src/Symfony/Component/Notifier/Bridge/Telnyx/README.md index e279e11167a35..36d1aa7a21efd 100644 --- a/src/Symfony/Component/Notifier/Bridge/Telnyx/README.md +++ b/src/Symfony/Component/Notifier/Bridge/Telnyx/README.md @@ -18,7 +18,7 @@ where: Resources --------- - * [Contributing](https://symfony.com/doc/current/contributing/index.html) - * [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Notifier/Bridge/Vonage/composer.json b/src/Symfony/Component/Notifier/Bridge/Vonage/composer.json index c80b016feaaa5..7607c77d8bda0 100644 --- a/src/Symfony/Component/Notifier/Bridge/Vonage/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Vonage/composer.json @@ -16,9 +16,9 @@ } ], "require": { - "php": ">=7.2.5", - "symfony/http-client": "^4.3|^5.0|^6.0", - "symfony/notifier": "^5.3|^6.0" + "php": ">=8.1", + "symfony/http-client": "^5.4|^6.0", + "symfony/notifier": "^5.4|^6.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Vonage\\": "" }, diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json index 07b3ecfb7c34b..1eff92d7fb4eb 100644 --- a/src/Symfony/Component/PropertyInfo/composer.json +++ b/src/Symfony/Component/PropertyInfo/composer.json @@ -32,7 +32,7 @@ "symfony/dependency-injection": "^5.4|^6.0", "phpdocumentor/reflection-docblock": "^5.2", "phpstan/phpdoc-parser": "^1.0", - "doctrine/annotations": "^1.10.4" + "doctrine/annotations": "^1.10.4|^2" }, "conflict": { "phpdocumentor/reflection-docblock": "<5.2", diff --git a/src/Symfony/Component/RateLimiter/Reservation.php b/src/Symfony/Component/RateLimiter/Reservation.php index 9e6029aa01251..f73b74bc263f7 100644 --- a/src/Symfony/Component/RateLimiter/Reservation.php +++ b/src/Symfony/Component/RateLimiter/Reservation.php @@ -45,6 +45,6 @@ public function getRateLimit(): RateLimit public function wait(): void { - usleep($this->getWaitDuration() * 1e6); + usleep((int) ($this->getWaitDuration() * 1e6)); } } diff --git a/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderWithAnnotationsTest.php b/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderWithAnnotationsTest.php index b7399df353ef0..e2843a0a39843 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderWithAnnotationsTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderWithAnnotationsTest.php @@ -26,7 +26,9 @@ protected function configureRoute(Route $route, \ReflectionClass $class, \Reflec { } }; - AnnotationRegistry::registerLoader('class_exists'); + if (method_exists(AnnotationRegistry::class, 'registerLoader')) { + AnnotationRegistry::registerLoader('class_exists'); + } } public function testDefaultRouteName() diff --git a/src/Symfony/Component/Routing/composer.json b/src/Symfony/Component/Routing/composer.json index 5c7c9cdb8cb23..2065447cbb0a8 100644 --- a/src/Symfony/Component/Routing/composer.json +++ b/src/Symfony/Component/Routing/composer.json @@ -24,7 +24,7 @@ "symfony/yaml": "^5.4|^6.0", "symfony/expression-language": "^5.4|^6.0", "symfony/dependency-injection": "^5.4|^6.0", - "doctrine/annotations": "^1.12", + "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3" }, "conflict": { diff --git a/src/Symfony/Component/Security/Core/User/UserInterface.php b/src/Symfony/Component/Security/Core/User/UserInterface.php index c9b0930b19ffc..cace8f6aed6cf 100644 --- a/src/Symfony/Component/Security/Core/User/UserInterface.php +++ b/src/Symfony/Component/Security/Core/User/UserInterface.php @@ -55,7 +55,7 @@ public function getRoles(): array; public function eraseCredentials(); /** - * Returns the identifier for this user (e.g. its username or email address). + * Returns the identifier for this user (e.g. username or email address). */ public function getUserIdentifier(): string; } diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticationSuccessHandlerInterface.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticationSuccessHandlerInterface.php index 8dad2b01eafc4..690234e710ba6 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticationSuccessHandlerInterface.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticationSuccessHandlerInterface.php @@ -27,9 +27,7 @@ interface AuthenticationSuccessHandlerInterface { /** - * This is called when an interactive authentication attempt succeeds. This - * is called by authentication listeners inheriting from - * AbstractAuthenticationListener. + * Usually called by AuthenticatorInterface::onAuthenticationSuccess() implementations. */ public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response; } diff --git a/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php b/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php index 41ec5dab590f3..9b898319a998c 100644 --- a/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php +++ b/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php @@ -89,8 +89,12 @@ public function consumeLoginLink(Request $request): UserInterface throw new InvalidLoginLinkException('User not found.', 0, $exception); } - $hash = $request->get('hash'); - $expires = $request->get('expires'); + if (!$hash = $request->get('hash')) { + throw new InvalidLoginLinkException('Missing "hash" parameter.'); + } + if (!$expires = $request->get('expires')) { + throw new InvalidLoginLinkException('Missing "expires" parameter.'); + } try { $this->signatureHasher->verifySignatureHash($user, $expires, $hash); diff --git a/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php index 697584d28b6d7..c29bb9a85fbde 100644 --- a/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php @@ -182,6 +182,30 @@ public function testConsumeLoginLinkExceedsMaxUsage() $linker->consumeLoginLink($request); } + public function testConsumeLoginLinkWithMissingHash() + { + $user = new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash'); + $this->userProvider->createUser($user); + + $this->expectException(InvalidLoginLinkException::class); + $request = Request::create('/login/verify?user=weaverryan&expires=10000'); + + $linker = $this->createLinker(); + $linker->consumeLoginLink($request); + } + + public function testConsumeLoginLinkWithMissingExpiration() + { + $user = new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash'); + $this->userProvider->createUser($user); + + $this->expectException(InvalidLoginLinkException::class); + $request = Request::create('/login/verify?user=weaverryan&hash=thehash'); + + $linker = $this->createLinker(); + $linker->consumeLoginLink($request); + } + private function createSignatureHash(string $username, int $expires, array $extraFields): string { $fields = [base64_encode($username), $expires]; diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index edbf08e296962..c5141e71452f5 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -130,17 +130,17 @@ protected function getAttributeValue(object $object, string $attribute, string $ $ucfirsted = ucfirst($attribute); $getter = 'get'.$ucfirsted; - if (\is_callable([$object, $getter])) { + if (method_exists($object, $getter) && \is_callable([$object, $getter])) { return $object->$getter(); } $isser = 'is'.$ucfirsted; - if (\is_callable([$object, $isser])) { + if (method_exists($object, $isser) && \is_callable([$object, $isser])) { return $object->$isser(); } $haser = 'has'.$ucfirsted; - if (\is_callable([$object, $haser])) { + if (method_exists($object, $haser) && \is_callable([$object, $haser])) { return $object->$haser(); } @@ -156,7 +156,14 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v $key = \get_class($object).':'.$setter; if (!isset(self::$setterAccessibleCache[$key])) { - self::$setterAccessibleCache[$key] = \is_callable([$object, $setter]) && !(new \ReflectionMethod($object, $setter))->isStatic(); + try { + // We have to use is_callable() here since method_exists() + // does not "see" protected/private methods + self::$setterAccessibleCache[$key] = \is_callable([$object, $setter]) && !(new \ReflectionMethod($object, $setter))->isStatic(); + } catch (\ReflectionException $e) { + // Method does not exist in the class, probably a magic method + self::$setterAccessibleCache[$key] = false; + } } if (self::$setterAccessibleCache[$key]) { diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 6fd430bb47a43..e6f8396fe9d15 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -453,6 +453,22 @@ public function testHasGetterNormalize() ); } + public function testCallMagicMethodDenormalize() + { + $obj = $this->normalizer->denormalize(['active' => true], ObjectWithMagicMethod::class); + $this->assertTrue($obj->isActive()); + } + + public function testCallMagicMethodNormalize() + { + $obj = new ObjectWithMagicMethod(); + + $this->assertSame( + ['active' => true], + $this->normalizer->normalize($obj, 'any') + ); + } + protected function getObjectCollectionWithExpectedArray(): array { return [[ @@ -722,3 +738,18 @@ public function hasFoo() return $this->foo; } } + +class ObjectWithMagicMethod +{ + private $active = true; + + public function isActive() + { + return $this->active; + } + + public function __call($key, $value) + { + throw new \RuntimeException('__call should not be called. Called with: '.$key); + } +} diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 5888e662d22d7..dfb0a928dadb8 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -20,7 +20,7 @@ "symfony/polyfill-ctype": "~1.8" }, "require-dev": { - "doctrine/annotations": "^1.12", + "doctrine/annotations": "^1.12|^2", "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", "symfony/cache": "^5.4|^6.0", "symfony/config": "^5.4|^6.0", @@ -42,7 +42,7 @@ "conflict": { "doctrine/annotations": "<1.12", "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/type-resolver": "<1.4.0|>=1.7.0", "symfony/dependency-injection": "<5.4", "symfony/property-access": "<5.4", "symfony/property-info": "<5.4", diff --git a/src/Symfony/Component/String/LazyString.php b/src/Symfony/Component/String/LazyString.php index 84fa7bf0b5a27..adbf4952b5a85 100644 --- a/src/Symfony/Component/String/LazyString.php +++ b/src/Symfony/Component/String/LazyString.php @@ -127,7 +127,7 @@ private static function getPrettyName(callable $callback): string } elseif ($callback instanceof \Closure) { $r = new \ReflectionFunction($callback); - if (str_contains($r->name, '{closure}') || !$class = $r->getClosureScopeClass()) { + if (str_contains($r->name, '{closure}') || !$class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { return $r->name; } diff --git a/src/Symfony/Component/Validator/Constraints/IbanValidator.php b/src/Symfony/Component/Validator/Constraints/IbanValidator.php index 3e9c15f7e115a..eaeb9aac40fc4 100644 --- a/src/Symfony/Component/Validator/Constraints/IbanValidator.php +++ b/src/Symfony/Component/Validator/Constraints/IbanValidator.php @@ -102,7 +102,7 @@ class IbanValidator extends ConstraintValidator 'MK' => 'MK\d{2}\d{3}[\dA-Z]{10}\d{2}', // Macedonia, Former Yugoslav Republic of 'ML' => 'ML\d{2}[A-Z]{1}\d{23}', // Mali 'MQ' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Martinique - 'MR' => 'MR13\d{5}\d{5}\d{11}\d{2}', // Mauritania + 'MR' => 'MR\d{2}\d{5}\d{5}\d{11}\d{2}', // Mauritania 'MT' => 'MT\d{2}[A-Z]{4}\d{5}[\dA-Z]{18}', // Malta 'MU' => 'MU\d{2}[A-Z]{4}\d{2}\d{2}\d{12}\d{3}[A-Z]{3}', // Mauritius 'MZ' => 'MZ\d{2}\d{21}', // Mozambique @@ -127,7 +127,7 @@ class IbanValidator extends ConstraintValidator 'SN' => 'SN\d{2}[A-Z]{1}\d{23}', // Senegal 'TF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // French Southern Territories 'TL' => 'TL\d{2}\d{3}\d{14}\d{2}', // Timor-Leste - 'TN' => 'TN59\d{2}\d{3}\d{13}\d{2}', // Tunisia + 'TN' => 'TN\d{2}\d{2}\d{3}\d{13}\d{2}', // Tunisia 'TR' => 'TR\d{2}\d{5}[\dA-Z]{1}[\dA-Z]{16}', // Turkey 'UA' => 'UA\d{2}\d{6}[\dA-Z]{19}', // Ukraine 'VA' => 'VA\d{2}\d{3}\d{15}', // Vatican City State diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 63cb9e3a9745e..2691295eada91 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -38,7 +38,7 @@ "symfony/property-access": "^5.4|^6.0", "symfony/property-info": "^5.4|^6.0", "symfony/translation": "^5.4|^6.0", - "doctrine/annotations": "^1.13", + "doctrine/annotations": "^1.13|^2", "egulias/email-validator": "^2.1.10|^3" }, "conflict": { diff --git a/src/Symfony/Component/VarDumper/Caster/Caster.php b/src/Symfony/Component/VarDumper/Caster/Caster.php index c2bbbd66ee14e..cd9b35f77b2ea 100644 --- a/src/Symfony/Component/VarDumper/Caster/Caster.php +++ b/src/Symfony/Component/VarDumper/Caster/Caster.php @@ -47,7 +47,7 @@ public static function castObject(object $obj, string $class, bool $hasDebugInfo if ($hasDebugInfo) { try { $debugInfo = $obj->__debugInfo(); - } catch (\Exception) { + } catch (\Throwable) { // ignore failing __debugInfo() $hasDebugInfo = false; } diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php index e12b78cea1ba2..f6c8c7410aed1 100644 --- a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php @@ -197,7 +197,7 @@ public static function castFunctionAbstract(\ReflectionFunctionAbstract $c, arra self::addMap($a, $c, [ 'returnsReference' => 'returnsReference', 'returnType' => 'getReturnType', - 'class' => 'getClosureScopeClass', + 'class' => \PHP_VERSION_ID >= 80111 ? 'getClosureCalledClass' : 'getClosureScopeClass', 'this' => 'getClosureThis', ]); diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/CasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/CasterTest.php index c39e82cf6adb0..66cd5fbeda660 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/CasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/CasterTest.php @@ -22,7 +22,7 @@ class CasterTest extends TestCase { use VarDumperTestTrait; - private $referenceArray = [ + private static $referenceArray = [ 'null' => null, 'empty' => false, 'public' => 'pub', @@ -38,12 +38,12 @@ class CasterTest extends TestCase public function testFilter($filter, $expectedDiff, $listedProperties = null) { if (null === $listedProperties) { - $filteredArray = Caster::filter($this->referenceArray, $filter); + $filteredArray = Caster::filter(self::$referenceArray, $filter); } else { - $filteredArray = Caster::filter($this->referenceArray, $filter, $listedProperties); + $filteredArray = Caster::filter(self::$referenceArray, $filter, $listedProperties); } - $this->assertSame($expectedDiff, array_diff_assoc($this->referenceArray, $filteredArray)); + $this->assertSame($expectedDiff, array_diff_assoc(self::$referenceArray, $filteredArray)); } public function provideFilter() @@ -126,7 +126,7 @@ public function provideFilter() ], [ Caster::EXCLUDE_NOT_IMPORTANT | Caster::EXCLUDE_VERBOSE, - $this->referenceArray, + self::$referenceArray, ['public', "\0*\0protected"], ], [ @@ -175,4 +175,14 @@ public function testAnonymousClass() , $c ); } + + public function testTypeErrorInDebugInfo() + { + $this->assertDumpMatchesFormat('class@anonymous {}', new class() { + public function __debugInfo(): array + { + return ['class' => \get_class(null)]; + } + }); + } } diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index f7ef22df5c241..b8388fb2a3ed9 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -151,6 +151,7 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount } if (null !== $sleep) { if (!isset($sleep[$n]) || ($i && $c !== $class)) { + unset($arrayValue[$name]); continue; } $sleep[$n] = false; @@ -166,6 +167,9 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount } } } + if (method_exists($class, '__unserialize')) { + $properties = $arrayValue; + } prepare_value: $objectsPool[$value] = [$id = \count($objectsPool)]; diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/__unserialize-but-no-__serialize.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/__unserialize-but-no-__serialize.php new file mode 100644 index 0000000000000..987999b8d2cfa --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/__unserialize-but-no-__serialize.php @@ -0,0 +1,15 @@ + 'ccc', + ], + ] +); diff --git a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php index c84f07e7acaeb..79da3e0e37ad9 100644 --- a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php +++ b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php @@ -233,6 +233,8 @@ public function provideExport() yield ['php74-serializable', new Php74Serializable()]; + yield ['__unserialize-but-no-__serialize', new __UnserializeButNo__Serialize()]; + yield ['unit-enum', [FooUnitEnum::Bar], true]; yield ['readonly', new FooReadonly('k', 'v')]; } @@ -437,3 +439,18 @@ public function unserialize($ser) class ArrayObject extends \ArrayObject { } + +class __UnserializeButNo__Serialize +{ + public $foo; + + public function __construct() + { + $this->foo = 'ccc'; + } + + public function __unserialize(array $data): void + { + $this->foo = $data['foo']; + } +} diff --git a/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php b/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php index 5c04414273815..47d5d6ff276f6 100644 --- a/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php +++ b/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Workflow\Dumper; -use InvalidArgumentException; use Symfony\Component\Workflow\Definition; use Symfony\Component\Workflow\Marking; use Symfony\Component\Workflow\Metadata\MetadataStoreInterface; @@ -57,7 +56,7 @@ class PlantUmlDumper implements DumperInterface public function __construct(string $transitionType) { if (!\in_array($transitionType, self::TRANSITION_TYPES, true)) { - throw new InvalidArgumentException("Transition type '$transitionType' does not exist."); + throw new \InvalidArgumentException("Transition type '$transitionType' does not exist."); } $this->transitionType = $transitionType; } @@ -209,9 +208,7 @@ private function getState(string $place, Definition $definition, Marking $markin $description = $workflowMetadata->getMetadata('description', $place); if (null !== $description) { - $output .= ' as '.$place. - \PHP_EOL. - $place.' : '.$description; + $output .= \PHP_EOL.$placeEscaped.' : '.$description; } return $output; diff --git a/src/Symfony/Component/Workflow/Tests/Dumper/PlantUmlDumperTest.php b/src/Symfony/Component/Workflow/Tests/Dumper/PlantUmlDumperTest.php index 85c67969b8488..0c750fc750255 100644 --- a/src/Symfony/Component/Workflow/Tests/Dumper/PlantUmlDumperTest.php +++ b/src/Symfony/Component/Workflow/Tests/Dumper/PlantUmlDumperTest.php @@ -12,9 +12,12 @@ namespace Symfony\Component\Workflow\Tests\Dumper; use PHPUnit\Framework\TestCase; +use Symfony\Component\Workflow\Definition; use Symfony\Component\Workflow\Dumper\PlantUmlDumper; use Symfony\Component\Workflow\Marking; +use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; use Symfony\Component\Workflow\Tests\WorkflowBuilderTrait; +use Symfony\Component\Workflow\Transition; class PlantUmlDumperTest extends TestCase { @@ -63,6 +66,34 @@ public function provideStateMachineDefinitionWithoutMarking() yield [$this->createComplexStateMachineDefinition(), $marking, 'complex-state-machine-marking', 'SimpleDiagram']; } + public function testDumpWorkflowWithSpacesInTheStateNamesAndDescription() + { + $dumper = new PlantUmlDumper(PlantUmlDumper::WORKFLOW_TRANSITION); + + // The graph looks like: + // + // +---------+ t 1 +----------+ | + // | place a | -----> | place b | | + // +---------+ +----------+ | + $places = ['place a', 'place b']; + + $transitions = []; + $transition = new Transition('t 1', 'place a', 'place b'); + $transitions[] = $transition; + + $placesMetadata = []; + $placesMetadata['place a'] = [ + 'description' => 'My custom place description', + ]; + $inMemoryMetadataStore = new InMemoryMetadataStore([], $placesMetadata); + $definition = new Definition($places, $transitions, null, $inMemoryMetadataStore); + + $dump = $dumper->dump($definition, null, ['title' => 'SimpleDiagram']); + $dump = str_replace(\PHP_EOL, "\n", $dump.\PHP_EOL); + $file = $this->getFixturePath('simple-workflow-with-spaces', PlantUmlDumper::WORKFLOW_TRANSITION); + $this->assertStringEqualsFile($file, $dump); + } + private function getFixturePath($name, $transitionType) { return __DIR__.'/../fixtures/puml/'.$transitionType.'/'.$name.'.puml'; diff --git a/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-marking.puml b/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-marking.puml index 0ea138f83f725..1e8a2ea0f6b86 100644 --- a/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-marking.puml +++ b/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-marking.puml @@ -17,8 +17,8 @@ skinparam agent { } state "a" <> state "b" <> -state "c" <> as c -c : My custom place description +state "c" <> +"c" : My custom place description agent "t1" agent "t2" "a" -[#Purple]-> "t1": "My custom transition label 2" diff --git a/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-nomarking.puml b/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-nomarking.puml index 02e7f396eacb3..b57dc5b1fab43 100644 --- a/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-nomarking.puml +++ b/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-nomarking.puml @@ -17,8 +17,8 @@ skinparam agent { } state "a" <> state "b" -state "c" <> as c -c : My custom place description +state "c" <> +"c" : My custom place description agent "t1" agent "t2" "a" -[#Purple]-> "t1": "My custom transition label 2" diff --git a/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-with-spaces.puml b/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-with-spaces.puml new file mode 100644 index 0000000000000..0e20d27198024 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/fixtures/puml/square/simple-workflow-with-spaces.puml @@ -0,0 +1,23 @@ +@startuml +allow_mixing +title SimpleDiagram +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam state { + BackgroundColor<> #87b741 + BackgroundColor<> #3887C6 + BorderColor #3887C6 + BorderColor<> Black + FontColor<> White +} +skinparam agent { + BackgroundColor #ffffff + BorderColor #3887C6 +} +state "place a" <> +"place a" : My custom place description +state "place b" +agent "t 1" +"place a" --> "t 1" +"t 1" --> "place b" +@enduml diff --git a/src/Symfony/Component/Yaml/Dumper.php b/src/Symfony/Component/Yaml/Dumper.php index c5024ff4e32bf..205da6fd2c575 100644 --- a/src/Symfony/Component/Yaml/Dumper.php +++ b/src/Symfony/Component/Yaml/Dumper.php @@ -56,6 +56,8 @@ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags if ($inline <= 0 || (!\is_array($input) && !$input instanceof TaggedValue && $dumpObjectAsInlineMap) || empty($input)) { $output .= $prefix.Inline::dump($input, $flags); + } elseif ($input instanceof TaggedValue) { + $output .= $this->dumpTaggedValue($input, $inline, $indent, $flags, $prefix); } else { $dumpAsMap = Inline::isHash($input); @@ -135,4 +137,28 @@ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags return $output; } + + private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, int $flags, string $prefix): string + { + $output = sprintf('%s!%s', $prefix ? $prefix.' ' : '', $value->getTag()); + + if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && false !== strpos($value->getValue(), "\n") && false === strpos($value->getValue(), "\r\n")) { + // If the first line starts with a space character, the spec requires a blockIndicationIndicator + // http://www.yaml.org/spec/1.2/spec.html#id2793979 + $blockIndentationIndicator = (' ' === substr($value->getValue(), 0, 1)) ? (string) $this->indentation : ''; + $output .= sprintf(' |%s', $blockIndentationIndicator); + + foreach (explode("\n", $value->getValue()) as $row) { + $output .= sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); + } + + return $output; + } + + if ($inline - 1 <= 0 || null === $value->getValue() || \is_scalar($value->getValue())) { + return $output.' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n"; + } + + return $output."\n".$this->dump($value->getValue(), $inline - 1, $indent, $flags); + } } diff --git a/src/Symfony/Component/Yaml/Tests/DumperTest.php b/src/Symfony/Component/Yaml/Tests/DumperTest.php index eedd3367d4e77..20308592f1c9b 100644 --- a/src/Symfony/Component/Yaml/Tests/DumperTest.php +++ b/src/Symfony/Component/Yaml/Tests/DumperTest.php @@ -440,6 +440,26 @@ public function testDumpingTaggedValueTopLevelAssocInline() $this->assertSameData($data, $this->parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS)); } + public function testDumpingTaggedValueTopLevelAssoc() + { + $data = new TaggedValue('user', ['name' => 'jane']); + + $expected = <<<'YAML' +!user +name: jane + +YAML; + $yaml = $this->dumper->dump($data, 2); + $this->assertSame($expected, $yaml); + } + + public function testDumpingTaggedValueTopLevelMultiLine() + { + $data = new TaggedValue('text', "a\nb\n"); + + $this->assertSame("!text |\n a\n b\n ", $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + } + public function testDumpingTaggedValueSpecialCharsInTag() { // @todo Validate the tag name in the TaggedValue constructor. diff --git a/src/Symfony/Contracts/Translation/Test/TranslatorTest.php b/src/Symfony/Contracts/Translation/Test/TranslatorTest.php index fbfa161929a4a..c5c37b355a5cb 100644 --- a/src/Symfony/Contracts/Translation/Test/TranslatorTest.php +++ b/src/Symfony/Contracts/Translation/Test/TranslatorTest.php @@ -137,7 +137,7 @@ public function getTransChoiceTests() } /** - * @dataProvider getInternal + * @dataProvider getInterval */ public function testInterval($expected, $number, $interval) { @@ -146,7 +146,7 @@ public function testInterval($expected, $number, $interval) $this->assertEquals($expected, $translator->trans($interval.' foo|[1,Inf[ bar', ['%count%' => $number])); } - public function getInternal() + public function getInterval() { return [ ['foo', 3, '{1,2, 3 ,4}'],