diff --git a/CHANGELOG-7.2.md b/CHANGELOG-7.2.md index 1125f1a72875d..0bb8758194576 100644 --- a/CHANGELOG-7.2.md +++ b/CHANGELOG-7.2.md @@ -7,6 +7,29 @@ in 7.2 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v7.2.0...v7.2.1 +* 7.2.5 (2025-03-28) + + * bug #60054 [Form] Use duplicate_preferred_choices to set value of ChoiceType (aleho) + * bug #60026 [Serializer] Fix ObjectNormalizer default context with named serializers (HypeMC) + * bug #60030 [Cache][DoctrineBridge][HttpFoundation][Lock][Messenger] use `Table::addPrimaryKeyConstraint()` with Doctrine DBAL 4.3+ (xabbuh) + * bug #59844 [TypeInfo] Fix `isSatisfiedBy` not traversing type tree (mtarld) + * bug #59858 Update `JsDelivrEsmResolver::IMPORT_REGEX` to support dynamic imports (natepage) + * bug #60019 [HttpKernel] Fix `TraceableEventDispatcher` when the `Stopwatch` service has been reset (lyrixx) + * bug #59975 [HttpKernel] Only remove `E_WARNING` from error level during kernel init (fritzmg) + * bug #59988 [FrameworkBundle] Remove redundant `name` attribute from `default_context` (HypeMC) + * bug #59963 [TypeInfo] Fix ``@var`` tag reading for promoted properties (mtarld) + * bug #59949 [Process] Use a pipe for stderr in pty mode to avoid mixed output between stdout and stderr (joelwurtz) + * bug #59940 [Cache] Fix missing cache data in profiler (dcmbrs) + * bug #59965 [VarExporter] Fix support for hooks and asymmetric visibility (nicolas-grekas) + * bug #59924 Extract no type ``@param`` annotation with `PhpStanExtractor` (thomasdubuffet) + * bug #59908 [Messenger] Reduce keepalive request noise (ro0NL) + * bug #59874 [Console] fix progress bar messing output in section when there is an EOL (joelwurtz) + * bug #59888 [PhpUnitBridge] don't trigger "internal" deprecations for PHPUnit Stub objects (xabbuh) + * bug #59830 [Yaml] drop comments while lexing unquoted strings (xabbuh) + * bug #59884 [VarExporter] Fix support for asymmetric visibility (nicolas-grekas) + * bug #59881 [VarExporter] Fix support for abstract properties (nicolas-grekas) + * bug #59841 [Cache] fix cache data collector on late collect (dcmbrs) + * 7.2.4 (2025-02-26) * bug #59198 [Messenger] Filter out non-consumable receivers when registering `ConsumeMessagesCommand` (wazum) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8af7f51d72c7d..f8902ba18f029 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -53,9 +53,9 @@ The Symfony Connect username in parenthesis allows to get more information - Mathieu Lechat (mat_the_cat) - Mathias Arlaud (mtarld) - Simon André (simonandre) + - Vincent Langlet (deviling) - Matthias Pigulla (mpdude) - Gabriel Ostrolucký (gadelat) - - Vincent Langlet (deviling) - Jonathan Wage (jwage) - Valentin Udaltsov (vudaltsov) - Grégoire Paris (greg0ire) @@ -73,9 +73,9 @@ The Symfony Connect username in parenthesis allows to get more information - Titouan Galopin (tgalopin) - Pierre du Plessis (pierredup) - David Maicher (dmaicher) + - Dariusz Ruminski - Tomasz Kowalczyk (thunderer) - Bulat Shakirzyanov (avalanche123) - - Dariusz Ruminski - Iltar van der Berg - Miha Vrhovnik (mvrhov) - Gary PEGEOT (gary-p) @@ -101,6 +101,7 @@ The Symfony Connect username in parenthesis allows to get more information - Tomas Norkūnas (norkunas) - Christian Raue - Eric Clemmons (ericclemmons) + - Hubert Lenoir (hubert_lenoir) - Denis (yethee) - Alex Pott - Michel Weimerskirch (mweimerskirch) @@ -110,7 +111,6 @@ The Symfony Connect username in parenthesis allows to get more information - Frank A. Fiebig (fafiebig) - Baldini - Fran Moreno (franmomu) - - Hubert Lenoir (hubert_lenoir) - Charles Sarrazin (csarrazi) - Henrik Westphal (snc) - Dariusz Górecki (canni) @@ -135,6 +135,7 @@ The Symfony Connect username in parenthesis allows to get more information - John Wards (johnwards) - Yanick Witschi (toflar) - Antoine Hérault (herzult) + - Valtteri R (valtzu) - Konstantin.Myakshin - Jeroen Spee (jeroens) - Arnaud Le Blanc (arnaud-lb) @@ -144,7 +145,6 @@ The Symfony Connect username in parenthesis allows to get more information - Tac Tacelosky (tacman1123) - gnito-org - Tim Nagel (merk) - - Valtteri R (valtzu) - Chris Wilkinson (thewilkybarkid) - Jérôme Vasseur (jvasseur) - Peter Kokot (peterkokot) @@ -194,6 +194,7 @@ The Symfony Connect username in parenthesis allows to get more information - Niels Keurentjes (curry684) - OGAWA Katsuhiro (fivestar) - Jhonny Lidfors (jhonne) + - Florent Morselli (spomky_) - Juti Noppornpitak (shiroyuki) - Gregor Harlan (gharlan) - Anthony MARTIN @@ -206,6 +207,7 @@ The Symfony Connect username in parenthesis allows to get more information - Thomas Landauer (thomas-landauer) - Arnaud Kleinpeter (nanocom) - Guilherme Blanco (guilhermeblanco) + - David Prévot (taffit) - Saif Eddin Gmati (azjezz) - Farhad Safarov (safarov) - SpacePossum @@ -217,8 +219,6 @@ The Symfony Connect username in parenthesis allows to get more information - Rafael Dohms (rdohms) - Roman Martinuk (a2a4) - jwdeitch - - David Prévot (taffit) - - Florent Morselli (spomky_) - Jérôme Parmentier (lctrs) - Ahmed TAILOULOUTE (ahmedtai) - Simon Berger @@ -317,6 +317,7 @@ The Symfony Connect username in parenthesis allows to get more information - Mathieu Lemoine (lemoinem) - Christian Schmidt - Andreas Hucks (meandmymonkey) + - Artem Lopata - Indra Gunawan (indragunawan) - Noel Guilbert (noel) - Bastien Jaillot (bastnic) @@ -352,6 +353,7 @@ The Symfony Connect username in parenthesis allows to get more information - John Kary (johnkary) - Võ Xuân Tiến (tienvx) - fd6130 (fdtvui) + - Antonio J. García Lagar (ajgarlag) - Priyadi Iman Nurcahyo (priyadi) - Alan Poulain (alanpoulain) - Oleg Andreyev (oleg.andreyev) @@ -388,6 +390,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dariusz - Daniel Gorgan - Francois Zaninotto + - Aurélien Pillevesse (aurelienpillevesse) - Daniel Tschinder - Christian Schmidt - Alexander Kotynia (olden) @@ -395,7 +398,6 @@ The Symfony Connect username in parenthesis allows to get more information - Manuel Reinhard (sprain) - Zan Baldwin (zanbaldwin) - Tim Goudriaan (codedmonkey) - - Antonio J. García Lagar (ajgarlag) - BoShurik - Quentin Devos - Adam Prager (padam87) @@ -409,7 +411,6 @@ The Symfony Connect username in parenthesis allows to get more information - Sylvain Fabre (sylfabre) - Xavier Perez - Arjen Brouwer (arjenjb) - - Artem Lopata - Patrick McDougle (patrick-mcdougle) - Arnt Gulbrandsen - Michel Roca (mroca) @@ -460,7 +461,6 @@ The Symfony Connect username in parenthesis allows to get more information - renanbr - Sébastien Lavoie (lavoiesl) - Alex Rock (pierstoval) - - Aurélien Pillevesse (aurelienpillevesse) - Matthieu Lempereur (mryamous) - Wodor Wodorski - Beau Simensen (simensen) @@ -514,6 +514,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ismael Ambrosi (iambrosi) - Craig Duncan (duncan3dc) - Emmanuel BORGES + - Mathieu Rochette (mathroc) - Karoly Negyesi (chx) - Aurelijus Valeiša (aurelijus) - Jan Decavele (jandc) @@ -540,6 +541,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ahmed Raafat - Philippe Segatori - Thibaut Cheymol (tcheymol) + - Raffaele Carelle - Erin Millard - Matthew Lewinski (lewinski) - Islam Israfilov (islam93) @@ -628,7 +630,6 @@ The Symfony Connect username in parenthesis allows to get more information - Saif Eddin G - Endre Fejes - Tobias Naumann (tna) - - Mathieu Rochette (mathroc) - Daniel Beyer - Ivan Sarastov (isarastov) - flack (flack) @@ -674,8 +675,10 @@ The Symfony Connect username in parenthesis allows to get more information - Benjamin (yzalis) - Jeanmonod David (jeanmonod) - Webnet team (webnet) + - Christian Gripp (core23) - Tobias Bönner - Nicolas Rigaud + - PHAS Developer - Ben Ramsey (ramsey) - Berny Cantos (xphere81) - Antonio Jose Cerezo (ajcerezo) @@ -692,7 +695,6 @@ The Symfony Connect username in parenthesis allows to get more information - Neil Peyssard (nepey) - Niklas Fiekas - Mark Challoner (markchalloner) - - Raffaele Carelle - Andreas Hennings - Markus Bachmann (baachi) - Gunnstein Lye (glye) @@ -755,6 +757,7 @@ The Symfony Connect username in parenthesis allows to get more information - Arnaud POINTET (oipnet) - Tristan Pouliquen - Miro Michalicka + - Hans Mackowiak - M. Vondano - Dominik Zogg - Maximilian Zumbansen @@ -948,7 +951,6 @@ The Symfony Connect username in parenthesis allows to get more information - Paul Oms - James Hemery - wuchen90 - - PHAS Developer - Wouter van der Loop (toppy-hennie) - Ninos - julien57 @@ -991,6 +993,7 @@ The Symfony Connect username in parenthesis allows to get more information - Noémi Salaün (noemi-salaun) - Sinan Eldem (sineld) - Gennady Telegin + - Benedikt Lenzen (demigodcode) - ampaze - Alexandre Dupuy (satchette) - Michel Hunziker @@ -1224,6 +1227,7 @@ The Symfony Connect username in parenthesis allows to get more information - Besnik Br - Issam Raouf (iraouf) - Simon Mönch + - Valmonzo - Sherin Bloemendaal - Jose Gonzalez - Jonathan (jlslew) @@ -1279,6 +1283,7 @@ The Symfony Connect username in parenthesis allows to get more information - Alexander Grimalovsky (flying) - Andrew Hilobok (hilobok) - Noah Heck (myesain) + - Sébastien JEAN (sebastien76) - Christian Soronellas (theunic) - Max Baldanza - Volodymyr Panivko @@ -1340,7 +1345,6 @@ The Symfony Connect username in parenthesis allows to get more information - James Michael DuPont - Tinjo Schöni - Carlos Buenosvinos (carlosbuenosvinos) - - Christian Gripp (core23) - Jake (jakesoft) - Rustam Bakeev (nommyde) - Vincent CHALAMON @@ -1548,8 +1552,10 @@ The Symfony Connect username in parenthesis allows to get more information - Guillaume Gammelin - Valérian Galliat - Sorin Pop (sorinpop) + - Elías Fernández - d-ph - Stewart Malik + - Frank Schulze (xit) - Renan Taranto (renan-taranto) - Ninos Ego - Samael tomas @@ -1635,6 +1641,7 @@ The Symfony Connect username in parenthesis allows to get more information - Michael H. Arieli - Miloš Milutinović - Jitendra Adhikari (adhocore) + - Kevin Jansen - Nicolas Martin (cocorambo) - Tom Panier (neemzy) - Fred Cox @@ -1650,6 +1657,7 @@ The Symfony Connect username in parenthesis allows to get more information - Adoni Pavlakis (adoni) - Nicolas Le Goff (nlegoff) - Maarten Nusteling (nusje2000) + - Peter van Dommelen - Anne-Sophie Bachelard - Gordienko Vladislav - Ahmed EBEN HASSINE (famas23) @@ -1762,6 +1770,7 @@ The Symfony Connect username in parenthesis allows to get more information - Asil Barkin Elik (asilelik) - Bhujagendra Ishaya - Guido Donnari + - Jérôme Dumas - Mert Simsek (mrtsmsk0) - Lin Clark - Christophe Meneses (c77men) @@ -1802,6 +1811,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dominic Luidold - Piotr Antosik (antek88) - Nacho Martin (nacmartin) + - Thomas Bibaut - Thibaut Chieux - mwos - Aydin Hassan @@ -1857,7 +1867,6 @@ The Symfony Connect username in parenthesis allows to get more information - Mikkel Paulson - Michał Strzelecki - Bert Ramakers - - Hans Mackowiak - Hugo Fonseca (fonsecas72) - Marc Duboc (icemad) - uncaught @@ -1942,11 +1951,13 @@ The Symfony Connect username in parenthesis allows to get more information - Joas Schilling - Ener-Getick - Markus Thielen + - Peter Trebaticky - Moza Bogdan (bogdan_moza) - Viacheslav Sychov - Nicolas Sauveur (baishu) - Helmut Hummel (helhum) - Matt Brunt + - David Vancl - Carlos Ortega Huetos - Péter Buri (burci) - Evgeny Efimov (edefimov) @@ -1982,10 +1993,14 @@ The Symfony Connect username in parenthesis allows to get more information - rchoquet - v.shevelev - rvoisin + - Dan Brown - gitlost - Taras Girnyk + - Simon Mönch + - Barthold Bos - cthulhu - Andoni Larzabal (andonilarz) + - Staormin - Dmitry Derepko - Rémi Leclerc - Jan Vernarsky @@ -2113,6 +2128,7 @@ The Symfony Connect username in parenthesis allows to get more information - martkop26 - Raphaël Davaillaud - Sander Hagen + - Alexander Menk - cilefen (cilefen) - Prasetyo Wicaksono (jowy) - Mo Di (modi) @@ -2293,6 +2309,7 @@ The Symfony Connect username in parenthesis allows to get more information - Willem Verspyck - Kim Laï Trinh - Johan de Ruijter + - InbarAbraham - Jason Desrosiers - m.chwedziak - marbul @@ -2348,6 +2365,7 @@ The Symfony Connect username in parenthesis allows to get more information - Phillip Look (plook) - Foxprodev - Artfaith + - Tom Kaminski - developer-av - Max Summe - Ema Panz @@ -2637,6 +2655,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jakub Simon - TheMhv - Eviljeks + - Juliano Petronetto - robin.de.croock - Brandon Antonio Lorenzo - Bouke Haarsma @@ -2826,6 +2845,7 @@ The Symfony Connect username in parenthesis allows to get more information - Botond Dani (picur) - Rémi Faivre (rfv) - Radek Wionczek (rwionczek) + - tinect (tinect) - Nick Stemerdink - Bernhard Rusch - David Stone @@ -3008,6 +3028,7 @@ The Symfony Connect username in parenthesis allows to get more information - Viet Pham - Alan Bondarchuk - Pchol + - Benjamin Ellis - Shamimul Alam - Cyril HERRERA - dropfen @@ -3305,7 +3326,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jimmy Leger (redpanda) - Ronny López (ronnylt) - Julius (sakalys) - - Sébastien JEAN (sebastien76) - Dmitry (staratel) - Marcin Szepczynski (szepczynski) - Tito Miguel Costa (titomiguelcosta) @@ -3578,6 +3598,7 @@ The Symfony Connect username in parenthesis allows to get more information - Thorsten Hallwas - Brian Freytag - Arend Hummeling + - Joseph FRANCLIN - Marco Pfeiffer - Alex Nostadt - Michael Squires @@ -3658,13 +3679,13 @@ The Symfony Connect username in parenthesis allows to get more information - Julia - Lin Lu - arduanov - - Valmonzo - sualko - Marc Bennewitz - Fabien - Martin Komischke - Yendric - ADmad + - Hugo Posnic - Nicolas Roudaire - Marc Jauvin - Matthias Meyer diff --git a/src/Symfony/Bridge/Doctrine/SchemaListener/AbstractSchemaListener.php b/src/Symfony/Bridge/Doctrine/SchemaListener/AbstractSchemaListener.php index 6f3410313d00a..cfe07b37da493 100644 --- a/src/Symfony/Bridge/Doctrine/SchemaListener/AbstractSchemaListener.php +++ b/src/Symfony/Bridge/Doctrine/SchemaListener/AbstractSchemaListener.php @@ -13,6 +13,9 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception\TableNotFoundException; +use Doctrine\DBAL\Schema\Name\Identifier; +use Doctrine\DBAL\Schema\Name\UnqualifiedName; +use Doctrine\DBAL\Schema\PrimaryKeyConstraint; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; @@ -30,7 +33,12 @@ protected function getIsSameDatabaseChecker(Connection $connection): \Closure $table->addColumn('id', Types::INTEGER) ->setAutoincrement(true) ->setNotnull(true); - $table->setPrimaryKey(['id']); + + if (class_exists(PrimaryKeyConstraint::class)) { + $table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, [new UnqualifiedName(Identifier::unquoted('id'))], true)); + } else { + $table->setPrimaryKey(['id']); + } $schemaManager->createTable($table); diff --git a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php index 251b011b5d44e..79cc0f0a31a4d 100644 --- a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php @@ -13,6 +13,9 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Schema\Name\Identifier; +use Doctrine\DBAL\Schema\Name\UnqualifiedName; +use Doctrine\DBAL\Schema\PrimaryKeyConstraint; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Types\Types; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; @@ -193,6 +196,11 @@ private function addTableToSchema(Schema $schema): void $table->addColumn('lastUsed', Types::DATETIME_IMMUTABLE); $table->addColumn('class', Types::STRING, ['length' => 100]); $table->addColumn('username', Types::STRING, ['length' => 200]); - $table->setPrimaryKey(['series']); + + if (class_exists(PrimaryKeyConstraint::class)) { + $table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, [new UnqualifiedName(Identifier::unquoted('series'))], true)); + } else { + $table->setPrimaryKey(['series']); + } } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index d6d00aa1fa0d3..d109785c2526a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -16,6 +16,7 @@ use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\PropertyAccessors\RawValuePropertyAccessor; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ObjectManager; @@ -132,11 +133,21 @@ class_exists(ClassMetadataInfo::class) ? ClassMetadataInfo::class : ClassMetadat ->willReturn(true) ; $refl = $this->createMock(\ReflectionProperty::class); + $refl + ->method('getName') + ->willReturn('name') + ; $refl ->method('getValue') ->willReturn(true) ; - $classMetadata->reflFields = ['name' => $refl]; + + if (property_exists(ClassMetadata::class, 'propertyAccessors')) { + $classMetadata->propertyAccessors['name'] = RawValuePropertyAccessor::fromReflectionProperty($refl); + } else { + $classMetadata->reflFields = ['name' => $refl]; + } + $em->expects($this->any()) ->method('getClassMetadata') ->willReturn($classMetadata) diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index 57dc44236e630..87eebbca142c6 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Doctrine\Validator\Constraints; +use Doctrine\ORM\Mapping\ClassMetadata as OrmClassMetadata; use Doctrine\ORM\Mapping\MappingException as ORMMappingException; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\Mapping\ClassMetadata; @@ -287,9 +288,13 @@ private function getFieldValues(mixed $object, ClassMetadata $class, array $fiel throw new ConstraintDefinitionException(\sprintf('The field "%s" is not a property of class "%s".', $fieldName, $objectClass)); } - $fieldValues[$entityFieldName] = $isValueEntity && $object instanceof ($class->getName()) - ? $class->reflFields[$fieldName]->getValue($object) - : $this->getPropertyValue($objectClass, $fieldName, $object); + if ($isValueEntity && $object instanceof ($class->getName()) && property_exists(OrmClassMetadata::class, 'propertyAccessors')) { + $fieldValues[$entityFieldName] = $class->propertyAccessors[$fieldName]->getValue($object); + } elseif ($isValueEntity && $object instanceof ($class->getName())) { + $fieldValues[$entityFieldName] = $class->reflFields[$fieldName]->getValue($object); + } else { + $fieldValues[$entityFieldName] = $this->getPropertyValue($objectClass, $fieldName, $object); + } } return $fieldValues; diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 1e421d5f9f5a9..537849faebaa4 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -89,7 +89,7 @@ {{- block('choice_widget_options') -}} {%- else -%} - + {%- endif -%} {% endfor %} {%- endblock choice_widget_options -%} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractDivLayoutTestCase.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractDivLayoutTestCase.php index cfa2c5c6475cf..28e8997a12e9f 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractDivLayoutTestCase.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractDivLayoutTestCase.php @@ -870,6 +870,56 @@ public function testMultipleChoiceExpandedWithLabelsSetFalseByCallable() ); } + public function testSingleChoiceWithoutDuplicatePreferredIsSelected() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&d', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c', 'Choice&D' => '&d'], + 'preferred_choices' => ['&b', '&d'], + 'duplicate_preferred_choices' => false, + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '-- sep --'], + '/select + [@name="name"] + [ + ./option[@value="&d"][@selected="selected"] + /following-sibling::option[@disabled="disabled"][.="-- sep --"] + /following-sibling::option[@value="&a"][not(@selected)] + /following-sibling::option[@value="&c"][not(@selected)] + ] + [count(./option)=5] +' + ); + } + + public function testSingleChoiceWithoutDuplicateNotPreferredIsSelected() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&d', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c', 'Choice&D' => '&d'], + 'preferred_choices' => ['&b', '&d'], + 'duplicate_preferred_choices' => true, + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '-- sep --'], + '/select + [@name="name"] + [ + ./option[@value="&d"][not(@selected)] + /following-sibling::option[@disabled="disabled"][.="-- sep --"] + /following-sibling::option[@value="&a"][not(@selected)] + /following-sibling::option[@value="&b"][not(@selected)] + /following-sibling::option[@value="&c"][not(@selected)] + /following-sibling::option[@value="&d"][@selected="selected"] + ] + [count(./option)=7] +' + ); + } + public function testFormEndWithRest() { $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TwigNodeProvider.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TwigNodeProvider.php index a6910855e38b1..64ce92bc04355 100644 --- a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TwigNodeProvider.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TwigNodeProvider.php @@ -14,6 +14,7 @@ use Symfony\Bridge\Twig\Node\TransDefaultDomainNode; use Symfony\Bridge\Twig\Node\TransNode; use Twig\Node\BodyNode; +use Twig\Node\EmptyNode; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; @@ -27,13 +28,15 @@ class TwigNodeProvider { public static function getModule($content) { + $emptyNodeExists = class_exists(EmptyNode::class); + return new ModuleNode( new BodyNode([new ConstantExpression($content, 0)]), null, - new ArrayExpression([], 0), - new ArrayExpression([], 0), - new ArrayExpression([], 0), - null, + $emptyNodeExists ? new EmptyNode() : new ArrayExpression([], 0), + $emptyNodeExists ? new EmptyNode() : new ArrayExpression([], 0), + $emptyNodeExists ? new EmptyNode() : new ArrayExpression([], 0), + $emptyNodeExists ? new EmptyNode() : null, new Source('', '') ); } diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 3af8ccbb7ecce..f0ae491de58a1 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -30,7 +30,7 @@ "symfony/dependency-injection": "^6.4|^7.0", "symfony/emoji": "^7.1", "symfony/finder": "^6.4|^7.0", - "symfony/form": "^6.4|^7.0", + "symfony/form": "^6.4.20|^7.2.5", "symfony/html-sanitizer": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index bd27c27a6948c..8d64adeca341d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1946,24 +1946,14 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->setParameter('serializer.default_context', $defaultContext); } - if (!$container->hasDefinition('serializer.normalizer.object')) { - return; - } - - $arguments = $container->getDefinition('serializer.normalizer.object')->getArguments(); - $context = $arguments[6] ?? $defaultContext; - - if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) { - $context += ['circular_reference_handler' => new Reference($config['circular_reference_handler'])]; - $container->getDefinition('serializer.normalizer.object')->setArgument(5, null); + if ($config['circular_reference_handler'] ?? false) { + $container->setParameter('.serializer.circular_reference_handler', $config['circular_reference_handler']); } if ($config['max_depth_handler'] ?? false) { - $context += ['max_depth_handler' => new Reference($config['max_depth_handler'])]; + $container->setParameter('.serializer.max_depth_handler', $config['max_depth_handler']); } - $container->getDefinition('serializer.normalizer.object')->setArgument(6, $context); - $container->getDefinition('serializer.normalizer.property')->setArgument(5, $defaultContext); $container->setParameter('.serializer.named_serializers', $config['named_serializers'] ?? []); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index 4686a88f662d6..b291f51ac8546 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -129,7 +129,7 @@ service('property_info')->ignoreOnInvalid(), service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(), null, - null, + abstract_arg('default context, set in the SerializerPass'), service('property_info')->ignoreOnInvalid(), ]) ->tag('serializer.normalizer', ['built_in' => true, 'priority' => -1000]) diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/HttpClientAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/HttpClientAssertionsTrait.php index 9d22a822fb851..4a8afbab4ab98 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/HttpClientAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/HttpClientAssertionsTrait.php @@ -14,10 +14,9 @@ use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector; -/* +/** * @author Mathieu Santostefano */ - trait HttpClientAssertionsTrait { public static function assertHttpClientRequest(string $expectedUrl, string $expectedMethod = 'GET', string|array|null $expectedBody = null, array $expectedHeaders = [], string $httpClientId = 'http_client'): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/NotificationAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/NotificationAssertionsTrait.php index b68473561eb4d..2c4c5467d4ebd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/NotificationAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/NotificationAssertionsTrait.php @@ -17,7 +17,7 @@ use Symfony\Component\Notifier\Message\MessageInterface; use Symfony\Component\Notifier\Test\Constraint as NotifierConstraint; -/* +/** * @author Smaïne Milianni */ trait NotificationAssertionsTrait diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 5f5f418010663..7bf66512d2b2b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -33,6 +33,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveTaggedIteratorArgumentPass; @@ -67,6 +68,7 @@ use Symfony\Component\Notifier\TexterInterface; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Security\Core\AuthenticationEvents; +use Symfony\Component\Serializer\DependencyInjection\SerializerPass; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; @@ -1447,9 +1449,6 @@ public function testSerializerEnabled() $this->assertEquals(AttributeLoader::class, $argument[0]->getClass()); $this->assertEquals(new Reference('serializer.name_converter.camel_case_to_snake_case'), $container->getDefinition('serializer.name_converter.metadata_aware')->getArgument(1)); $this->assertEquals(new Reference('property_info', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE), $container->getDefinition('serializer.normalizer.object')->getArgument(3)); - $this->assertArrayHasKey('circular_reference_handler', $container->getDefinition('serializer.normalizer.object')->getArgument(6)); - $this->assertArrayHasKey('max_depth_handler', $container->getDefinition('serializer.normalizer.object')->getArgument(6)); - $this->assertEquals($container->getDefinition('serializer.normalizer.object')->getArgument(6)['max_depth_handler'], new Reference('my.max.depth.handler')); } public function testSerializerWithoutTranslator() @@ -1547,13 +1546,22 @@ public function testJsonSerializableNormalizerRegistered() public function testObjectNormalizerRegistered() { - $container = $this->createContainerFromFile('full'); + $container = $this->createContainerFromFile('full', compile: false); + $container->addCompilerPass(new SerializerPass()); + $container->addCompilerPass(new ResolveBindingsPass()); + $container->compile(); $definition = $container->getDefinition('serializer.normalizer.object'); $tag = $definition->getTag('serializer.normalizer'); $this->assertEquals(ObjectNormalizer::class, $definition->getClass()); $this->assertEquals(-1000, $tag[0]['priority']); + + $this->assertEquals([ + 'enable_max_depth' => true, + 'circular_reference_handler' => new Reference('my.circular.reference.handler'), + 'max_depth_handler' => new Reference('my.max.depth.handler'), + ], $definition->getArgument(6)); } public function testConstraintViolationListNormalizerRegistered() diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 9b3e7c86ea3ff..6689b61b05990 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -59,7 +59,7 @@ "symfony/scheduler": "^6.4.4|^7.0.4", "symfony/security-bundle": "^6.4|^7.0", "symfony/semaphore": "^6.4|^7.0", - "symfony/serializer": "^7.1", + "symfony/serializer": "^7.2.5", "symfony/stopwatch": "^6.4|^7.0", "symfony/string": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", @@ -97,7 +97,7 @@ "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", "symfony/security-csrf": "<7.2", "symfony/security-core": "<6.4", - "symfony/serializer": "<7.1", + "symfony/serializer": "<7.2.5", "symfony/stopwatch": "<6.4", "symfony/translation": "<6.4", "symfony/twig-bridge": "<6.4", diff --git a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php b/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php index f2e4da9b96b49..b88f0e792d44f 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php @@ -28,7 +28,7 @@ final class JsDelivrEsmResolver implements PackageResolverInterface public const URL_PATTERN_DIST = self::URL_PATTERN_DIST_CSS.'/+esm'; public const URL_PATTERN_ENTRYPOINT = 'https://data.jsdelivr.com/v1/packages/npm/%s@%s/entrypoints'; - public const IMPORT_REGEX = '#(?:import\s*(?:[\w$]+,)?(?:(?:\{[^}]*\}|[\w$]+|\*\s*as\s+[\w$]+)\s*\bfrom\s*)?|export\s*(?:\{[^}]*\}|\*)\s*from\s*)("/npm/((?:@[^/]+/)?[^@]+?)(?:@([^/]+))?((?:/[^/]+)*?)/\+esm")#'; + public const IMPORT_REGEX = '#(?:import\s*(?:[\w$]+,)?(?:(?:\{[^}]*\}|[\w$]+|\*\s*as\s+[\w$]+)\s*\bfrom\s*)?|export\s*(?:\{[^}]*\}|\*)\s*from\s*|await\simport\()("/npm/((?:@[^/]+/)?[^@]+?)(?:@([^/]+))?((?:/[^/]+)*?)/\+esm")(?:\)*)#'; private const ES_MODULE_SHIMS = 'es-module-shims'; diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php index 8b7d82c8c6f06..d9650fd7c29d3 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php @@ -693,6 +693,13 @@ public static function provideImportRegex(): iterable ['jquery', '3.7.0'], ], ]; + + yield 'dynamic import with path' => [ + 'return(await import("/npm/@datadog/browser-rum@6.3.0/esm/boot/startRecording.js/+esm")).startRecording', + [ + ['@datadog/browser-rum/esm/boot/startRecording.js', '6.3.0'], + ], + ]; } private static function createRemoteEntry(string $importName, string $version, ImportMapType $type = ImportMapType::JS, ?string $packageSpecifier = null): ImportMapEntry diff --git a/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php b/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php index c69c777c993e7..d67464a4fd560 100644 --- a/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php @@ -19,6 +19,9 @@ use Doctrine\DBAL\Exception\TableNotFoundException; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use Doctrine\DBAL\Schema\Name\Identifier; +use Doctrine\DBAL\Schema\Name\UnqualifiedName; +use Doctrine\DBAL\Schema\PrimaryKeyConstraint; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Tools\DsnParser; use Symfony\Component\Cache\Exception\InvalidArgumentException; @@ -378,6 +381,11 @@ private function addTableToSchema(Schema $schema): void $table->addColumn($this->dataCol, 'blob', ['length' => 16777215]); $table->addColumn($this->lifetimeCol, 'integer', ['unsigned' => true, 'notnull' => false]); $table->addColumn($this->timeCol, 'integer', ['unsigned' => true]); - $table->setPrimaryKey([$this->idCol]); + + if (class_exists(PrimaryKeyConstraint::class)) { + $table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, [new UnqualifiedName(Identifier::unquoted($this->idCol))], true)); + } else { + $table->setPrimaryKey([$this->idCol]); + } } } diff --git a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php index b9bcdaf132572..22a5a0391673f 100644 --- a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php +++ b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php @@ -38,15 +38,7 @@ public function addInstance(string $name, TraceableAdapter $instance): void public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { - $empty = ['calls' => [], 'adapters' => [], 'config' => [], 'options' => [], 'statistics' => []]; - $this->data = ['instances' => $empty, 'total' => $empty]; - foreach ($this->instances as $name => $instance) { - $this->data['instances']['calls'][$name] = $instance->getCalls(); - $this->data['instances']['adapters'][$name] = get_debug_type($instance->getPool()); - } - - $this->data['instances']['statistics'] = $this->calculateStatistics(); - $this->data['total']['statistics'] = $this->calculateTotalStatistics(); + $this->lateCollect(); } public function reset(): void @@ -59,6 +51,15 @@ public function reset(): void public function lateCollect(): void { + $empty = ['calls' => [], 'adapters' => [], 'config' => [], 'options' => [], 'statistics' => []]; + $this->data = ['instances' => $empty, 'total' => $empty]; + foreach ($this->instances as $name => $instance) { + $this->data['instances']['calls'][$name] = $instance->getCalls(); + $this->data['instances']['adapters'][$name] = get_debug_type($instance->getPool()); + } + + $this->data['instances']['statistics'] = $this->calculateStatistics(); + $this->data['total']['statistics'] = $this->calculateTotalStatistics(); $this->data['instances']['calls'] = $this->cloneVar($this->data['instances']['calls']); } diff --git a/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php b/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php index bb102363cf758..cea761f5f99ac 100644 --- a/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php +++ b/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Cache\DataCollector\CacheDataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\VarDumper\Cloner\Data; class CacheDataCollectorTest extends TestCase { @@ -104,6 +105,27 @@ public function testCollectBeforeEnd() $this->assertSame(1, $stats[self::INSTANCE_NAME]['misses'], 'misses'); } + public function testLateCollect() + { + $adapter = new TraceableAdapter(new NullAdapter()); + + $collector = new CacheDataCollector(); + $collector->addInstance(self::INSTANCE_NAME, $adapter); + + $adapter->get('foo', function () use ($collector) { + $collector->lateCollect(); + + return 123; + }); + + $stats = $collector->getStatistics(); + $this->assertGreaterThan(0, $stats[self::INSTANCE_NAME]['time']); + $this->assertEquals($stats[self::INSTANCE_NAME]['hits'], 0, 'hits'); + $this->assertEquals($stats[self::INSTANCE_NAME]['misses'], 1, 'misses'); + $this->assertEquals($stats[self::INSTANCE_NAME]['calls'], 1, 'calls'); + $this->assertInstanceOf(Data::class, $collector->getCalls()); + } + private function getCacheDataCollectorStatisticsFromEvents(array $traceableAdapterEvents) { $traceableAdapterMock = $this->createMock(TraceableAdapter::class); diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 49e34d8d8c9e8..dc3605ad2fee0 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -513,12 +513,21 @@ private function overwrite(string $message): void if ($this->output instanceof ConsoleSectionOutput) { $messageLines = explode("\n", $this->previousMessage); $lineCount = \count($messageLines); + + $lastLineWithoutDecoration = Helper::removeDecoration($this->output->getFormatter(), end($messageLines) ?? ''); + + // When the last previous line is empty (without formatting) it is already cleared by the section output, so we don't need to clear it again + if ('' === $lastLineWithoutDecoration) { + --$lineCount; + } + foreach ($messageLines as $messageLine) { $messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine)); if ($messageLineLength > $this->terminal->getWidth()) { $lineCount += floor($messageLineLength / $this->terminal->getWidth()); } } + $this->output->clear($lineCount); } else { $lineCount = substr_count($this->previousMessage, "\n"); diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 3d1bfa48fce27..0df42e738a6f6 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -416,6 +416,81 @@ public function testOverwriteWithSectionOutput() ); } + public function testOverwriteWithSectionOutputAndEol() + { + $sections = []; + $stream = $this->getOutputStream(true); + $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + + $bar = new ProgressBar($output, 50, 0); + $bar->setFormat('[%bar%] %percent:3s%%' . PHP_EOL . '%message%' . PHP_EOL); + $bar->setMessage(''); + $bar->start(); + $bar->display(); + $bar->setMessage('Doing something...'); + $bar->advance(); + $bar->setMessage('Doing something foo...'); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals(escapeshellcmd( + '[>---------------------------] 0%'.\PHP_EOL.\PHP_EOL. + "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL. 'Doing something...' . \PHP_EOL . + "\x1b[2A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL. 'Doing something foo...' . \PHP_EOL), + escapeshellcmd(stream_get_contents($output->getStream())) + ); + } + + public function testOverwriteWithSectionOutputAndEolWithEmptyMessage() + { + $sections = []; + $stream = $this->getOutputStream(true); + $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + + $bar = new ProgressBar($output, 50, 0); + $bar->setFormat('[%bar%] %percent:3s%%' . PHP_EOL . '%message%'); + $bar->setMessage('Start'); + $bar->start(); + $bar->display(); + $bar->setMessage(''); + $bar->advance(); + $bar->setMessage('Doing something...'); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals(escapeshellcmd( + '[>---------------------------] 0%'.\PHP_EOL.'Start'.\PHP_EOL. + "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL . + "\x1b[1A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL. 'Doing something...' . \PHP_EOL), + escapeshellcmd(stream_get_contents($output->getStream())) + ); + } + + public function testOverwriteWithSectionOutputAndEolWithEmptyMessageComment() + { + $sections = []; + $stream = $this->getOutputStream(true); + $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + + $bar = new ProgressBar($output, 50, 0); + $bar->setFormat('[%bar%] %percent:3s%%' . PHP_EOL . '%message%'); + $bar->setMessage('Start'); + $bar->start(); + $bar->display(); + $bar->setMessage(''); + $bar->advance(); + $bar->setMessage('Doing something...'); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals(escapeshellcmd( + '[>---------------------------] 0%'.\PHP_EOL."\x1b[33mStart\x1b[39m".\PHP_EOL. + "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL . + "\x1b[1A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL. "\x1b[33mDoing something...\x1b[39m" . \PHP_EOL), + escapeshellcmd(stream_get_contents($output->getStream())) + ); + } + public function testOverwriteWithAnsiSectionOutput() { // output has 43 visible characters plus 2 invisible ANSI characters diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php index 58ad91e764076..b9e9164573672 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php @@ -76,7 +76,7 @@ class WitherProxy1991f2a extends \Symfony\Component\DependencyInjection\Tests\Co use \Symfony\Component\VarExporter\LazyProxyTrait; private const LAZY_OBJECT_PROPERTY_SCOPES = [ - 'foo' => [parent::class, 'foo', null], + 'foo' => [parent::class, 'foo', null, 4], ]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php index 0df7e0c98e274..d70588f655329 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php @@ -78,7 +78,7 @@ class WitherProxyE94fdba extends \Symfony\Component\DependencyInjection\Tests\Co use \Symfony\Component\VarExporter\LazyProxyTrait; private const LAZY_OBJECT_PROPERTY_SCOPES = [ - 'foo' => [parent::class, 'foo', null], + 'foo' => [parent::class, 'foo', null, 4], ]; } diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index b5fda9bdeb990..460751088f451 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -20,7 +20,7 @@ "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^3.5", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "require-dev": { "symfony/yaml": "^6.4|^7.0", diff --git a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php index 39ee082391d88..3a2ef4fb7d525 100644 --- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php +++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php @@ -18,6 +18,7 @@ use Phake\IMock; use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation; use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Stub; use Prophecy\Prophecy\ProphecySubjectInterface; use ProxyManager\Proxy\ProxyInterface; use Symfony\Component\DependencyInjection\Argument\LazyClosure; @@ -257,6 +258,7 @@ public static function checkClasses(): bool for (; $i < \count($symbols); ++$i) { if (!is_subclass_of($symbols[$i], MockObject::class) + && !is_subclass_of($symbols[$i], Stub::class) && !is_subclass_of($symbols[$i], ProphecySubjectInterface::class) && !is_subclass_of($symbols[$i], Proxy::class) && !is_subclass_of($symbols[$i], ProxyInterface::class) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 35a3175924ed4..fc083ee40d516 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -275,6 +275,8 @@ public function buildView(FormView $view, FormInterface $form, array $options): public function finishView(FormView $view, FormInterface $form, array $options): void { + $view->vars['duplicate_preferred_choices'] = $options['duplicate_preferred_choices']; + if ($options['expanded']) { // Radio buttons should have the same name as the parent $childName = $view->vars['full_name']; diff --git a/src/Symfony/Component/Form/Resources/translations/validators.es.xlf b/src/Symfony/Component/Form/Resources/translations/validators.es.xlf index 301e2b33f7ed3..a9989737c33eb 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.es.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.es.xlf @@ -52,7 +52,7 @@ Please enter a valid date. - Por favor, ingrese una fecha valida. + Por favor, ingrese una fecha válida. Please select a valid file. diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index f08471e9b9130..e2fb4f129a124 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -11,6 +11,9 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; +use Doctrine\DBAL\Schema\Name\Identifier; +use Doctrine\DBAL\Schema\Name\UnqualifiedName; +use Doctrine\DBAL\Schema\PrimaryKeyConstraint; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Types\Types; @@ -224,7 +227,13 @@ public function configureSchema(Schema $schema, ?\Closure $isSameDatabase = null default: throw new \DomainException(\sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); } - $table->setPrimaryKey([$this->idCol]); + + if (class_exists(PrimaryKeyConstraint::class)) { + $table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, [new UnqualifiedName(Identifier::unquoted($this->idCol))], true)); + } else { + $table->setPrimaryKey([$this->idCol]); + } + $table->addIndex([$this->lifetimeCol], $this->lifetimeCol.'_idx'); } diff --git a/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php index 9c4f4981a5bba..beca6bfb149a1 100644 --- a/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php @@ -66,7 +66,11 @@ protected function afterDispatch(string $eventName, object $event): void if (null === $sectionId) { break; } - $this->stopwatch->stopSection($sectionId); + try { + $this->stopwatch->stopSection($sectionId); + } catch (\LogicException) { + // The stop watch service might have been reset in the meantime + } break; case KernelEvents::TERMINATE: // In the special case described in the `preDispatch` method above, the `$token` section diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index ead55c9272043..d2e1eda84c5e8 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -73,11 +73,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.2.4'; - public const VERSION_ID = 70204; + public const VERSION = '7.2.5'; + public const VERSION_ID = 70205; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 2; - public const RELEASE_VERSION = 4; + public const RELEASE_VERSION = 5; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2025'; @@ -399,7 +399,8 @@ protected function initializeContainer(): void $cachePath = $cache->getPath(); // Silence E_WARNING to ignore "include" failures - don't use "@" to prevent silencing fatal errors - $errorLevel = error_reporting(\E_ALL ^ \E_WARNING); + $errorLevel = error_reporting(); + error_reporting($errorLevel & ~\E_WARNING); try { if (is_file($cachePath) && \is_object($this->container = include $cachePath) diff --git a/src/Symfony/Component/Lock/Store/DoctrineDbalStore.php b/src/Symfony/Component/Lock/Store/DoctrineDbalStore.php index dc2d5ee39bd06..f042620b71a6b 100644 --- a/src/Symfony/Component/Lock/Store/DoctrineDbalStore.php +++ b/src/Symfony/Component/Lock/Store/DoctrineDbalStore.php @@ -18,6 +18,9 @@ use Doctrine\DBAL\Exception\TableNotFoundException; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use Doctrine\DBAL\Schema\Name\Identifier; +use Doctrine\DBAL\Schema\Name\UnqualifiedName; +use Doctrine\DBAL\Schema\PrimaryKeyConstraint; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Tools\DsnParser; use Symfony\Component\Lock\Exception\InvalidArgumentException; @@ -214,7 +217,12 @@ public function configureSchema(Schema $schema, \Closure $isSameDatabase): void $table->addColumn($this->idCol, 'string', ['length' => 64]); $table->addColumn($this->tokenCol, 'string', ['length' => 44]); $table->addColumn($this->expirationCol, 'integer', ['unsigned' => true]); - $table->setPrimaryKey([$this->idCol]); + + if (class_exists(PrimaryKeyConstraint::class)) { + $table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, [new UnqualifiedName(Identifier::unquoted($this->idCol))], true)); + } else { + $table->setPrimaryKey([$this->idCol]); + } } /** diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php index e0b06cfd7ef8c..4901824a85c1b 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php @@ -23,6 +23,9 @@ use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\AbstractAsset; +use Doctrine\DBAL\Schema\Name\Identifier; +use Doctrine\DBAL\Schema\Name\UnqualifiedName; +use Doctrine\DBAL\Schema\PrimaryKeyConstraint; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Types\Types; @@ -519,7 +522,11 @@ private function addTableToSchema(Schema $schema): void ->setNotnull(true); $table->addColumn('delivered_at', Types::DATETIME_IMMUTABLE) ->setNotnull(false); - $table->setPrimaryKey(['id']); + if (class_exists(PrimaryKeyConstraint::class)) { + $table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, [new UnqualifiedName(Identifier::unquoted('id'))], true)); + } else { + $table->setPrimaryKey(['id']); + } $table->addIndex(['queue_name']); $table->addIndex(['available_at']); $table->addIndex(['delivered_at']); diff --git a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php index f1b7d5a7ec836..1a84381008318 100644 --- a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php +++ b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php @@ -290,7 +290,7 @@ public function handleSignal(int $signal, int|false $previousExitCode = 0): int| } if (\SIGALRM === $signal) { - $this->logger?->info('Sending keepalive request.', ['transport_names' => $this->worker->getMetadata()->getTransportNames()]); + $this->logger?->debug('Sending keepalive request.', ['transport_names' => $this->worker->getMetadata()->getTransportNames()]); $this->worker->keepalive($this->getApplication()->getAlarmInterval()); diff --git a/src/Symfony/Component/Process/Pipes/UnixPipes.php b/src/Symfony/Component/Process/Pipes/UnixPipes.php index 8e95afaafb877..6f95a3328ffd1 100644 --- a/src/Symfony/Component/Process/Pipes/UnixPipes.php +++ b/src/Symfony/Component/Process/Pipes/UnixPipes.php @@ -70,7 +70,7 @@ public function getDescriptors(): array return [ ['pty'], ['pty'], - ['pty'], + ['pipe', 'w'], // stderr needs to be in a pipe to correctly split error and output, since PHP will use the same stream for both ]; } diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index b17bfc7a31aa0..00d06208eb6a8 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -559,6 +559,20 @@ public function testExitCodeTextIsNullWhenExitCodeIsNull() $this->assertNull($process->getExitCodeText()); } + public function testStderrNotMixedWithStdout() + { + if (!Process::isPtySupported()) { + $this->markTestSkipped('PTY is not supported on this operating system.'); + } + + $process = $this->getProcess('echo "foo" && echo "bar" >&2'); + $process->setPty(true); + $process->run(); + + $this->assertSame("foo\r\n", $process->getOutput()); + $this->assertSame("bar\n", $process->getErrorOutput()); + } + public function testPTYCommand() { if (!Process::isPtySupported()) { diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php index cbf634933511a..f5f83d968f7ba 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php @@ -16,6 +16,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; @@ -206,7 +208,7 @@ public function getType(string $class, string $property, array $context = []): ? $types = []; foreach ($docNode->getTagsByName($tag) as $tagDocNode) { - if ($tagDocNode->value instanceof InvalidTagValueNode) { + if (!$tagDocNode->value instanceof ParamTagValueNode && !$tagDocNode->value instanceof ReturnTagValueNode && !$tagDocNode->value instanceof VarTagValueNode) { continue; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 0d77497c2e1da..d7aaac1b226a7 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -694,6 +694,7 @@ public static function invalidTypesProvider(): iterable yield 'stat' => ['stat']; yield 'foo' => ['foo']; yield 'bar' => ['bar']; + yield 'baz' => ['baz']; } /** diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/InvalidDummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/InvalidDummy.php index 0a4cc81f36be8..0940c287b072d 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/InvalidDummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/InvalidDummy.php @@ -47,4 +47,11 @@ public function getBar() { return 'bar'; } + + /** + * @param $baz + */ + public function setBaz($baz) + { + } } diff --git a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php index 7b7f6f1c2313b..179b7a3d92e9d 100644 --- a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php +++ b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php @@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Serializer\Debug\TraceableEncoder; use Symfony\Component\Serializer\Debug\TraceableNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\SerializerInterface; /** @@ -54,17 +55,27 @@ public function process(ContainerBuilder $container): void throw new RuntimeException('You must tag at least one service as "serializer.encoder" to use the "serializer" service.'); } + $defaultContext = []; if ($container->hasParameter('serializer.default_context')) { $defaultContext = $container->getParameter('serializer.default_context'); - $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $defaultContext); $container->getParameterBag()->remove('serializer.default_context'); $container->getDefinition('serializer')->setArgument('$defaultContext', $defaultContext); } + /** @var ?string $circularReferenceHandler */ + $circularReferenceHandler = $container->hasParameter('.serializer.circular_reference_handler') + ? $container->getParameter('.serializer.circular_reference_handler') : null; + + /** @var ?string $maxDepthHandler */ + $maxDepthHandler = $container->hasParameter('.serializer.max_depth_handler') + ? $container->getParameter('.serializer.max_depth_handler') : null; + + $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $defaultContext, $circularReferenceHandler, $maxDepthHandler); + $this->configureSerializer($container, 'serializer', $normalizers, $encoders, 'default'); if ($namedSerializers) { - $this->configureNamedSerializers($container); + $this->configureNamedSerializers($container, $circularReferenceHandler, $maxDepthHandler); } } @@ -98,11 +109,22 @@ private function createNamedSerializerTags(ContainerBuilder $container, string $ } } - private function bindDefaultContext(ContainerBuilder $container, array $services, array $defaultContext): void + private function bindDefaultContext(ContainerBuilder $container, array $services, array $defaultContext, ?string $circularReferenceHandler, ?string $maxDepthHandler): void { foreach ($services as $id) { $definition = $container->getDefinition((string) $id); - $definition->setBindings(['array $defaultContext' => new BoundArgument($defaultContext, false)] + $definition->getBindings()); + + $context = $defaultContext; + if (is_a($definition->getClass(), ObjectNormalizer::class, true)) { + if (null !== $circularReferenceHandler) { + $context += ['circular_reference_handler' => new Reference($circularReferenceHandler)]; + } + if (null !== $maxDepthHandler) { + $context += ['max_depth_handler' => new Reference($maxDepthHandler)]; + } + } + + $definition->setBindings(['array $defaultContext' => new BoundArgument($context, false)] + $definition->getBindings()); } } @@ -125,7 +147,7 @@ private function configureSerializer(ContainerBuilder $container, string $id, ar $serializerDefinition->replaceArgument(1, $encoders); } - private function configureNamedSerializers(ContainerBuilder $container): void + private function configureNamedSerializers(ContainerBuilder $container, ?string $circularReferenceHandler, ?string $maxDepthHandler): void { $defaultSerializerNameConverter = $container->hasParameter('.serializer.name_converter') ? $container->getParameter('.serializer.name_converter') : null; @@ -149,7 +171,7 @@ private function configureNamedSerializers(ContainerBuilder $container): void $normalizers = $this->buildChildDefinitions($container, $serializerName, $normalizers, $config); $encoders = $this->buildChildDefinitions($container, $serializerName, $encoders, $config); - $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $config['default_context']); + $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $config['default_context'], $circularReferenceHandler, $maxDepthHandler); $container->registerChild($serializerId, 'serializer')->setArgument('$defaultContext', $config['default_context']); $container->registerAliasForArgument($serializerId, SerializerInterface::class, $serializerName.'.serializer'); @@ -184,7 +206,9 @@ private function buildChildDefinitions(ContainerBuilder $container, string $seri foreach ($services as &$id) { $childId = $id.'.'.$serializerName; - $definition = $container->registerChild($childId, (string) $id); + $definition = $container->registerChild($childId, (string) $id) + ->setClass($container->getDefinition((string) $id)->getClass()) + ; if (null !== $nameConverterIndex = $this->findNameConverterIndex($container, (string) $id)) { $definition->replaceArgument($nameConverterIndex, new Reference($config['name_converter'])); diff --git a/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php b/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php index 769243be25f88..88ec02b87c57d 100644 --- a/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php +++ b/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Debug\TraceableNormalizer; use Symfony\Component\Serializer\Debug\TraceableSerializer; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\SerializerInterface; /** @@ -99,6 +100,32 @@ public function testBindSerializerDefaultContext() $this->assertEquals($context, $container->getDefinition('serializer')->getArgument('$defaultContext')); } + /** + * @testWith [{}, {}] + * [{"serializer.default_context": {"enable_max_depth": true}}, {"enable_max_depth": true}] + * [{".serializer.circular_reference_handler": "foo"}, {"circular_reference_handler": "foo"}] + * [{".serializer.max_depth_handler": "bar"}, {"max_depth_handler": "bar"}] + * [{"serializer.default_context": {"enable_max_depth": true}, ".serializer.circular_reference_handler": "foo", ".serializer.max_depth_handler": "bar"}, {"enable_max_depth": true, "circular_reference_handler": "foo", "max_depth_handler": "bar"}] + */ + public function testBindObjectNormalizerDefaultContext(array $parameters, array $context) + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->register('serializer')->setArguments([null, null, []]); + $container->getParameterBag()->add($parameters); + $definition = $container->register('serializer.normalizer.object') + ->setClass(ObjectNormalizer::class) + ->addTag('serializer.normalizer') + ->addTag('serializer.encoder') + ; + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $bindings = $definition->getBindings(); + $this->assertEquals($bindings['array $defaultContext'], new BoundArgument($context, false)); + } + public function testNormalizersAndEncodersAreDecoratedAndOrderedWhenCollectingData() { $container = new ContainerBuilder(); @@ -565,7 +592,9 @@ public function testBindSerializerDefaultContextToNamedSerializers() $serializerPass = new SerializerPass(); $serializerPass->process($container); - $this->assertEmpty($definition->getBindings()); + $bindings = $definition->getBindings(); + $this->assertArrayHasKey('array $defaultContext', $bindings); + $this->assertEquals($bindings['array $defaultContext'], new BoundArgument([], false)); $bindings = $container->getDefinition('n1.api')->getBindings(); $this->assertArrayHasKey('array $defaultContext', $bindings); @@ -574,6 +603,37 @@ public function testBindSerializerDefaultContextToNamedSerializers() $this->assertEquals($defaultContext, $container->getDefinition('serializer.api')->getArgument('$defaultContext')); } + /** + * @testWith [{}, {}, {}] + * [{"enable_max_depth": true}, {}, {"enable_max_depth": true}] + * [{}, {".serializer.circular_reference_handler": "foo"}, {"circular_reference_handler": "foo"}] + * [{}, {".serializer.max_depth_handler": "bar"}, {"max_depth_handler": "bar"}] + * [{"enable_max_depth": true}, {".serializer.circular_reference_handler": "foo", ".serializer.max_depth_handler": "bar"}, {"enable_max_depth": true, "circular_reference_handler": "foo", "max_depth_handler": "bar"}] + */ + public function testBindNamedSerializerObjectNormalizerDefaultContext(array $defaultContext, array $parameters, array $context) + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', [ + 'api' => ['default_context' => $defaultContext], + ]); + + $container->register('serializer')->setArguments([null, null, []]); + $container->getParameterBag()->add($parameters); + $container->register('serializer.normalizer.object') + ->setClass(ObjectNormalizer::class) + ->addTag('serializer.normalizer', ['serializer' => '*']) + ->addTag('serializer.encoder', ['serializer' => '*']) + ; + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $bindings = $container->getDefinition('serializer.normalizer.object.api')->getBindings(); + $this->assertArrayHasKey('array $defaultContext', $bindings); + $this->assertEquals($bindings['array $defaultContext'], new BoundArgument($context, false)); + } + public function testNamedSerializersAreRegistered() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php index 30141f95c3d0f..035457d12a95f 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php @@ -11,9 +11,18 @@ final class DummyWithPhpDoc /** * @param bool $promoted + * @param bool $promotedVarAndParam */ public function __construct( public mixed $promoted, + /** + * @var string + */ + public mixed $promotedVar, + /** + * @var string + */ + public mixed $promotedVarAndParam, ) { } diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php index 7e92638a9ce38..996b04ff11238 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php @@ -29,6 +29,8 @@ public function testReadPhpDoc() $this->assertEquals(Type::array(Type::object(Dummy::class)), $resolver->resolve($reflection->getProperty('arrayOfDummies'))); $this->assertEquals(Type::bool(), $resolver->resolve($reflection->getProperty('promoted'))); + $this->assertEquals(Type::string(), $resolver->resolve($reflection->getProperty('promotedVar'))); + $this->assertEquals(Type::string(), $resolver->resolve($reflection->getProperty('promotedVarAndParam'))); $this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy'))); $this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy')->getParameters()[0])); } diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeTest.php index 6d60b5dc21eca..8b2bc4d900c48 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeTest.php @@ -13,6 +13,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\UnionType; use Symfony\Component\TypeInfo\TypeIdentifier; class TypeTest extends TestCase @@ -34,4 +36,12 @@ public function testIsNullable() $this->assertFalse(Type::int()->isNullable()); } + + public function testIsSatisfiedBy() + { + $this->assertTrue(Type::union(Type::int(), Type::string())->isSatisfiedBy(fn (Type $t): bool => 'int' === (string) $t)); + $this->assertTrue(Type::union(Type::int(), Type::string())->isSatisfiedBy(fn (Type $t): bool => $t instanceof UnionType)); + $this->assertTrue(Type::list(Type::int())->isSatisfiedBy(fn (Type $t): bool => $t instanceof CollectionType && 'int' === (string) $t->getCollectionValueType())); + $this->assertFalse(Type::list(Type::int())->isSatisfiedBy(fn (Type $t): bool => 'int' === (string) $t)); + } } diff --git a/src/Symfony/Component/TypeInfo/Type.php b/src/Symfony/Component/TypeInfo/Type.php index 2a39f14e7b5bf..c14b86263a784 100644 --- a/src/Symfony/Component/TypeInfo/Type.php +++ b/src/Symfony/Component/TypeInfo/Type.php @@ -29,6 +29,14 @@ abstract class Type implements \Stringable */ public function isSatisfiedBy(callable $specification): bool { + if ($this instanceof WrappingTypeInterface && $this->wrappedTypeIsSatisfiedBy($specification)) { + return true; + } + + if ($this instanceof CompositeTypeInterface && $this->composedTypesAreSatisfiedBy($specification)) { + return true; + } + return $specification($this); } @@ -37,19 +45,17 @@ public function isSatisfiedBy(callable $specification): bool */ public function isIdentifiedBy(TypeIdentifier|string ...$identifiers): bool { - $specification = static function (Type $type) use (&$specification, $identifiers): bool { - if ($type instanceof WrappingTypeInterface) { - return $type->wrappedTypeIsSatisfiedBy($specification); - } + $specification = static fn (Type $type): bool => $type->isIdentifiedBy(...$identifiers); - if ($type instanceof CompositeTypeInterface) { - return $type->composedTypesAreSatisfiedBy($specification); - } + if ($this instanceof WrappingTypeInterface && $this->wrappedTypeIsSatisfiedBy($specification)) { + return true; + } - return $type->isIdentifiedBy(...$identifiers); - }; + if ($this instanceof CompositeTypeInterface && $this->composedTypesAreSatisfiedBy($specification)) { + return true; + } - return $this->isSatisfiedBy($specification); + return false; } public function isNullable(): bool diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php index 9f71ee4bc2ed8..24aa20c7d7d72 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php @@ -64,36 +64,38 @@ public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type throw new UnsupportedException(\sprintf('Expected subject to be a "ReflectionProperty", a "ReflectionParameter" or a "ReflectionFunctionAbstract", "%s" given.', get_debug_type($subject)), $subject); } - $docComment = match (true) { - $subject instanceof \ReflectionProperty => $subject->isPromoted() ? $subject->getDeclaringClass()?->getConstructor()?->getDocComment() : $subject->getDocComment(), - $subject instanceof \ReflectionParameter => $subject->getDeclaringFunction()->getDocComment(), - $subject instanceof \ReflectionFunctionAbstract => $subject->getDocComment(), + $typeContext ??= $this->typeContextFactory->createFromReflection($subject); + + $docComments = match (true) { + $subject instanceof \ReflectionProperty => $subject->isPromoted() + ? ['@var' => $subject->getDocComment(), '@param' => $subject->getDeclaringClass()?->getConstructor()?->getDocComment()] + : ['@var' => $subject->getDocComment()], + $subject instanceof \ReflectionParameter => ['@param' => $subject->getDeclaringFunction()->getDocComment()], + $subject instanceof \ReflectionFunctionAbstract => ['@return' => $subject->getDocComment()], }; - if (!$docComment) { - return $this->reflectionTypeResolver->resolve($subject); - } + foreach ($docComments as $tagName => $docComment) { + if (!$docComment) { + continue; + } - $typeContext ??= $this->typeContextFactory->createFromReflection($subject); + $tokens = new TokenIterator($this->lexer->tokenize($docComment)); + $docNode = $this->phpDocParser->parse($tokens); - $tagName = match (true) { - $subject instanceof \ReflectionProperty => $subject->isPromoted() ? '@param' : '@var', - $subject instanceof \ReflectionParameter => '@param', - $subject instanceof \ReflectionFunctionAbstract => '@return', - }; + foreach ($docNode->getTagsByName($tagName) as $tag) { + $tagValue = $tag->value; - $tokens = new TokenIterator($this->lexer->tokenize($docComment)); - $docNode = $this->phpDocParser->parse($tokens); + if ('@var' === $tagName && $tagValue instanceof VarTagValueNode) { + return $this->stringTypeResolver->resolve((string) $tagValue, $typeContext); + } - foreach ($docNode->getTagsByName($tagName) as $tag) { - $tagValue = $tag->value; + if ('@param' === $tagName && $tagValue instanceof ParamTagValueNode && '$'.$subject->getName() === $tagValue->parameterName) { + return $this->stringTypeResolver->resolve((string) $tagValue, $typeContext); + } - if ( - $tagValue instanceof VarTagValueNode - || $tagValue instanceof ParamTagValueNode && $tagName && '$'.$subject->getName() === $tagValue->parameterName - || $tagValue instanceof ReturnTagValueNode - ) { - return $this->stringTypeResolver->resolve((string) $tagValue, $typeContext); + if ('@return' === $tagName && $tagValue instanceof ReturnTagValueNode) { + return $this->stringTypeResolver->resolve((string) $tagValue, $typeContext); + } } } diff --git a/src/Symfony/Component/Validator/Constraints/All.php b/src/Symfony/Component/Validator/Constraints/All.php index 1da939dd5559f..1284545849ac0 100644 --- a/src/Symfony/Component/Validator/Constraints/All.php +++ b/src/Symfony/Component/Validator/Constraints/All.php @@ -25,8 +25,8 @@ class All extends Composite public array|Constraint $constraints = []; /** - * @param array|array|null $constraints - * @param string[]|null $groups + * @param array|array|Constraint|null $constraints + * @param string[]|null $groups */ public function __construct(mixed $constraints = null, ?array $groups = null, mixed $payload = null) { diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf index c4b36fd45f7f0..f6d2e0c28a33e 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf @@ -444,31 +444,31 @@ This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. - この値は短すぎます。少なくとも 1 つの単語を含める必要があります。|この値は短すぎます。少なくとも {{ min }} 個の単語を含める必要があります。 + この値は短すぎます。{{ min }}単語以上にする必要があります。 This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. - この値は長すぎます。1 つの単語のみを含める必要があります。|この値は長すぎます。{{ max }} 個以下の単語を含める必要があります。 + この値は長すぎます。{{ max }}単語以下にする必要があります。 This value does not represent a valid week in the ISO 8601 format. - この値は ISO 8601 形式の有効な週を表していません。 + この値は ISO 8601 形式の有効な週を表していません。 This value is not a valid week. - この値は有効な週ではありません。 + この値は有効な週ではありません。 This value should not be before week "{{ min }}". - この値は週 "{{ min }}" より前であってはなりません。 + この値は週 "{{ min }}" より前であってはいけません。 This value should not be after week "{{ max }}". - この値は週 "{{ max }}" 以降であってはなりません。 + この値は週 "{{ max }}" 以降であってはいけません。 This value is not a valid slug. - この値は有効なスラグではありません。 + この値は有効なスラグではありません。 diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.uk.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.uk.xlf index f83a179b1c37a..50d503e2455e7 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.uk.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.uk.xlf @@ -180,7 +180,7 @@ This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters. - Значення повиино бути рівним {{ limit }} символу.|Значення повиино бути рівним {{ limit }} символам.|Значення повиино бути рівним {{ limit }} символам. + Значення повинно бути рівним {{ limit }} символу.|Значення повинно бути рівним {{ limit }} символам.|Значення повинно бути рівним {{ limit }} символам. The file was only partially uploaded. diff --git a/src/Symfony/Component/VarExporter/Hydrator.php b/src/Symfony/Component/VarExporter/Hydrator.php index 5f456fb3cf7e7..b718921d9f892 100644 --- a/src/Symfony/Component/VarExporter/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Hydrator.php @@ -61,8 +61,8 @@ public static function hydrate(object $instance, array $properties = [], array $ $propertyScopes = InternalHydrator::$propertyScopes[$class] ??= InternalHydrator::getPropertyScopes($class); foreach ($properties as $name => &$value) { - [$scope, $name, $readonlyScope] = $propertyScopes[$name] ?? [$class, $name, $class]; - $scopedProperties[$readonlyScope ?? $scope][$name] = &$value; + [$scope, $name, $writeScope] = $propertyScopes[$name] ?? [$class, $name, $class]; + $scopedProperties[$writeScope ?? $scope][$name] = &$value; } unset($value); } diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index 6544ae244837a..75954dbf3e759 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -90,7 +90,8 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount $properties = $serializeProperties; } else { foreach ($serializeProperties as $n => $v) { - $c = $reflector->hasProperty($n) && ($p = $reflector->getProperty($n))->isReadOnly() ? $p->class : 'stdClass'; + $p = $reflector->hasProperty($n) ? $reflector->getProperty($n) : null; + $c = $p && (\PHP_VERSION_ID >= 80400 ? $p->isProtectedSet() || $p->isPrivateSet() : $p->isReadOnly()) ? $p->class : 'stdClass'; $properties[$c][$n] = $v; } } @@ -143,7 +144,8 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount $i = 0; $n = (string) $name; if ('' === $n || "\0" !== $n[0]) { - $c = $reflector->hasProperty($n) && ($p = $reflector->getProperty($n))->isReadOnly() ? $p->class : 'stdClass'; + $p = $reflector->hasProperty($n) ? $reflector->getProperty($n) : null; + $c = $p && (\PHP_VERSION_ID >= 80400 ? $p->isProtectedSet() || $p->isPrivateSet() : $p->isReadOnly()) ? $p->class : 'stdClass'; } elseif ('*' === $n[1]) { $n = substr($n, 3); $c = $reflector->getProperty($n)->class; diff --git a/src/Symfony/Component/VarExporter/Internal/Hydrator.php b/src/Symfony/Component/VarExporter/Internal/Hydrator.php index 38d643fc438ea..bc3df10f36f15 100644 --- a/src/Symfony/Component/VarExporter/Internal/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Internal/Hydrator.php @@ -20,6 +20,9 @@ */ class Hydrator { + public const PROPERTY_HAS_HOOKS = 1; + public const PROPERTY_NOT_BY_REF = 2; + public static array $hydrators = []; public static array $simpleHydrators = []; public static array $propertyScopes = []; @@ -150,13 +153,16 @@ public static function getHydrator($class) public static function getSimpleHydrator($class) { $baseHydrator = self::$simpleHydrators['stdClass'] ??= (function ($properties, $object) { - $readonly = (array) $this; + $notByRef = (array) $this; foreach ($properties as $name => &$value) { - $object->$name = $value; - - if (!($readonly[$name] ?? false)) { + if (!$noRef = $notByRef[$name] ?? false) { + $object->$name = $value; $object->$name = &$value; + } elseif (true !== $noRef) { + $notByRef($object, $value); + } else { + $object->$name = $value; } } })->bindTo(new \stdClass()); @@ -211,14 +217,19 @@ public static function getSimpleHydrator($class) } if (!$classReflector->isInternal()) { - $readonly = new \stdClass(); - foreach ($classReflector->getProperties(\ReflectionProperty::IS_READONLY) as $propertyReflector) { - if ($class === $propertyReflector->class) { - $readonly->{$propertyReflector->name} = true; + $notByRef = new \stdClass(); + foreach ($classReflector->getProperties() as $propertyReflector) { + if ($propertyReflector->isStatic()) { + continue; + } + if (\PHP_VERSION_ID >= 80400 && !$propertyReflector->isAbstract() && $propertyReflector->getHooks()) { + $notByRef->{$propertyReflector->name} = $propertyReflector->setRawValue(...); + } elseif ($propertyReflector->isReadOnly()) { + $notByRef->{$propertyReflector->name} = true; } } - return $baseHydrator->bindTo($readonly, $class); + return $baseHydrator->bindTo($notByRef, $class); } if ($classReflector->name !== $class) { @@ -260,25 +271,26 @@ public static function getPropertyScopes($class): array continue; } $name = $property->name; - $readonlyScope = null; + $access = ($flags << 2) | ($flags & \ReflectionProperty::IS_READONLY ? self::PROPERTY_NOT_BY_REF : 0); + + if (\PHP_VERSION_ID >= 80400 && !$property->isAbstract() && $h = $property->getHooks()) { + $access |= self::PROPERTY_HAS_HOOKS | (isset($h['get']) && !$h['get']->returnsReference() ? self::PROPERTY_NOT_BY_REF : 0); + } if (\ReflectionProperty::IS_PRIVATE & $flags) { - if ($flags & \ReflectionProperty::IS_READONLY) { - $readonlyScope = $class; - } - $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $readonlyScope, $property]; + $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, null, $access, $property]; continue; } - if ($flags & \ReflectionProperty::IS_READONLY) { - $readonlyScope = $property->class; + + $propertyScopes[$name] = [$class, $name, null, $access, $property]; + + if ($flags & (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET : \ReflectionProperty::IS_READONLY)) { + $propertyScopes[$name][2] = $property->class; } - $propertyScopes[$name] = [$class, $name, $readonlyScope, $property]; if (\ReflectionProperty::IS_PROTECTED & $flags) { $propertyScopes["\0*\0$name"] = $propertyScopes[$name]; - } elseif (\PHP_VERSION_ID >= 80400 && $property->getHooks()) { - $propertyScopes[$name][] = true; } } @@ -286,12 +298,20 @@ public static function getPropertyScopes($class): array $class = $r->name; foreach ($r->getProperties(\ReflectionProperty::IS_PRIVATE) as $property) { - if (!$property->isStatic()) { - $name = $property->name; - $readonlyScope = $property->isReadOnly() ? $class : null; - $propertyScopes["\0$class\0$name"] = [$class, $name, $readonlyScope, $property]; - $propertyScopes[$name] ??= [$class, $name, $readonlyScope, $property]; + $flags = $property->getModifiers(); + + if (\ReflectionProperty::IS_STATIC & $flags) { + continue; + } + $name = $property->name; + $access = ($flags << 2) | ($flags & \ReflectionProperty::IS_READONLY ? self::PROPERTY_NOT_BY_REF : 0); + + if (\PHP_VERSION_ID >= 80400 && $h = $property->getHooks()) { + $access |= self::PROPERTY_HAS_HOOKS | (isset($h['get']) && !$h['get']->returnsReference() ? self::PROPERTY_NOT_BY_REF : 0); } + + $propertyScopes["\0$class\0$name"] = [$class, $name, null, $access, $property]; + $propertyScopes[$name] ??= $propertyScopes["\0$class\0$name"]; } } diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php index 2bd318ffd7e04..a2034258f0c8c 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php @@ -58,17 +58,17 @@ public static function getClassResetters($class) $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); } - foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) { + foreach ($propertyScopes as $key => [$scope, $name, $writeScope, $access]) { $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; if ($k !== $key || "\0$class\0lazyObjectState" === $k) { continue; } - if ($k === $name && ($propertyScopes[$k][4] ?? false)) { + if ($access & Hydrator::PROPERTY_HAS_HOOKS) { $hookedProperties[$k] = true; } else { - $classProperties[$readonlyScope ?? $scope][$name] = $key; + $classProperties[$writeScope ?? $scope][$name] = $key; } } @@ -89,8 +89,8 @@ public static function getClassResetters($class) public static function getClassAccessors($class) { return \Closure::bind(static fn () => [ - 'get' => static function &($instance, $name, $readonly) { - if (!$readonly) { + 'get' => static function &($instance, $name, $notByRef) { + if (!$notByRef) { return $instance->$name; } $value = $instance->$name; @@ -126,9 +126,9 @@ public static function getParentMethods($class) return $methods; } - public static function getScope($propertyScopes, $class, $property, $readonlyScope = null) + public static function getScopeForRead($propertyScopes, $class, $property) { - if (null === $readonlyScope && !isset($propertyScopes[$k = "\0$class\0$property"]) && !isset($propertyScopes[$k = "\0*\0$property"])) { + if (!isset($propertyScopes[$k = "\0$class\0$property"]) && !isset($propertyScopes[$k = "\0*\0$property"])) { return null; } $frame = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]; @@ -136,7 +136,27 @@ public static function getScope($propertyScopes, $class, $property, $readonlySco if (\ReflectionProperty::class === $scope = $frame['class'] ?? \Closure::class) { $scope = $frame['object']->class; } - if (null === $readonlyScope && '*' === $k[1] && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) { + if ('*' === $k[1] && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) { + return null; + } + + return $scope; + } + + public static function getScopeForWrite($propertyScopes, $class, $property, $flags) + { + if (!($flags & (\ReflectionProperty::IS_PRIVATE | \ReflectionProperty::IS_PROTECTED | \ReflectionProperty::IS_READONLY | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET | \ReflectionProperty::IS_PROTECTED_SET : 0)))) { + return null; + } + $frame = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]; + + if (\ReflectionProperty::class === $scope = $frame['class'] ?? \Closure::class) { + $scope = $frame['object']->class; + } + if ($flags & (\ReflectionProperty::IS_PRIVATE | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET : \ReflectionProperty::IS_READONLY))) { + return $scope; + } + if ($flags & (\ReflectionProperty::IS_PROTECTED | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PROTECTED_SET : 0)) && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) { return null; } diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php index 30fbff18028ca..ac16d13b82acb 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php @@ -43,7 +43,7 @@ public function __construct( ) { } - public function initialize($instance, $propertyName, $propertyScope) + public function initialize($instance, $propertyName, $writeScope) { if (self::STATUS_UNINITIALIZED_FULL !== $this->status) { return $this->status; @@ -74,10 +74,10 @@ public function reset($instance): void $skippedProperties = $this->skippedProperties; $properties = (array) $instance; - foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) { + foreach ($propertyScopes as $key => [$scope, $name, , $access]) { $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; - if ($k === $key && (null !== $readonlyScope || !\array_key_exists($k, $properties))) { + if ($k === $key && ($access & Hydrator::PROPERTY_HAS_HOOKS || ($access >> 2) & \ReflectionProperty::IS_READONLY || !\array_key_exists($k, $properties))) { $skippedProperties[$k] = true; } } diff --git a/src/Symfony/Component/VarExporter/LazyGhostTrait.php b/src/Symfony/Component/VarExporter/LazyGhostTrait.php index 72a96d42449ae..2d90ca33c3e81 100644 --- a/src/Symfony/Component/VarExporter/LazyGhostTrait.php +++ b/src/Symfony/Component/VarExporter/LazyGhostTrait.php @@ -122,22 +122,28 @@ public function &__get($name): mixed { $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; + $notByRef = 0; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForRead($propertyScopes, $class, $name); $state = $this->lazyObjectState ?? null; if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))) { + $notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF; + if (LazyObjectState::STATUS_INITIALIZED_FULL === $state->status) { // Work around php/php-src#12695 $property = null === $scope ? $name : "\0$scope\0$name"; - $property = $propertyScopes[$property][3] - ?? Hydrator::$propertyScopes[$this::class][$property][3] = new \ReflectionProperty($scope ?? $class, $name); + $property = $propertyScopes[$property][4] + ?? Hydrator::$propertyScopes[$this::class][$property][4] = new \ReflectionProperty($scope ?? $class, $name); } else { $property = null; } + if (\PHP_VERSION_ID >= 80400 && !$notByRef && ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET) { + $scope ??= $writeScope; + } - if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope)) { + if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)) { goto get_in_scope; } } @@ -161,7 +167,7 @@ public function &__get($name): mixed try { if (null === $scope) { - if (null === $readonlyScope) { + if (!$notByRef) { return $this->$name; } $value = $this->$name; @@ -170,7 +176,7 @@ public function &__get($name): mixed } $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); - return $accessor['get']($this, $name, null !== $readonlyScope); + return $accessor['get']($this, $name, $notByRef); } catch (\Error $e) { if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) { throw $e; @@ -185,7 +191,7 @@ public function &__get($name): mixed $accessor['set']($this, $name, []); - return $accessor['get']($this, $name, null !== $readonlyScope); + return $accessor['get']($this, $name, $notByRef); } catch (\Error) { if (preg_match('/^Cannot access uninitialized non-nullable property ([^ ]++) by reference$/', $e->getMessage(), $matches)) { throw new \Error('Typed property '.$matches[1].' must not be accessed before initialization', $e->getCode(), $e->getPrevious()); @@ -201,15 +207,15 @@ public function __set($name, $value): void $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2); $state = $this->lazyObjectState ?? null; - if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) + if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status ) { if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) { - $state->initialize($this, $name, $readonlyScope ?? $scope); + $state->initialize($this, $name, $writeScope ?? $scope); } goto set_in_scope; } @@ -236,13 +242,13 @@ public function __isset($name): bool $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name); + if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForRead($propertyScopes, $class, $name); $state = $this->lazyObjectState ?? null; if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"])) && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status - && LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope) + && LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope) ) { goto isset_in_scope; } @@ -267,15 +273,15 @@ public function __unset($name): void $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2); $state = $this->lazyObjectState ?? null; - if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) + if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status ) { if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) { - $state->initialize($this, $name, $readonlyScope ?? $scope); + $state->initialize($this, $name, $writeScope ?? $scope); } goto unset_in_scope; } diff --git a/src/Symfony/Component/VarExporter/LazyProxyTrait.php b/src/Symfony/Component/VarExporter/LazyProxyTrait.php index 795e4d77d530d..3c59b73fdba8b 100644 --- a/src/Symfony/Component/VarExporter/LazyProxyTrait.php +++ b/src/Symfony/Component/VarExporter/LazyProxyTrait.php @@ -105,14 +105,19 @@ public function &__get($name): mixed $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; $instance = $this; + $notByRef = 0; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF; + $scope = Registry::getScopeForRead($propertyScopes, $class, $name); if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) { if ($state = $this->lazyObjectState ?? null) { $instance = $state->realInstance ??= ($state->initializer)(); } + if (\PHP_VERSION_ID >= 80400 && !$notByRef && ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET) { + $scope ??= $writeScope; + } $parent = 2; goto get_in_scope; } @@ -136,10 +141,11 @@ public function &__get($name): mixed } get_in_scope: + $notByRef = $notByRef || 1 === $parent; try { if (null === $scope) { - if (null === $readonlyScope && 1 !== $parent) { + if (!$notByRef) { return $instance->$name; } $value = $instance->$name; @@ -148,7 +154,7 @@ public function &__get($name): mixed } $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); - return $accessor['get']($instance, $name, null !== $readonlyScope || 1 === $parent); + return $accessor['get']($instance, $name, $notByRef); } catch (\Error $e) { if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) { throw $e; @@ -163,7 +169,7 @@ public function &__get($name): mixed $accessor['set']($instance, $name, []); - return $accessor['get']($instance, $name, null !== $readonlyScope || 1 === $parent); + return $accessor['get']($instance, $name, $notByRef); } catch (\Error) { throw $e; } @@ -176,10 +182,10 @@ public function __set($name, $value): void $scope = null; $instance = $this; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2); - if ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { + if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { if ($state = $this->lazyObjectState ?? null) { $instance = $state->realInstance ??= ($state->initializer)(); } @@ -212,7 +218,7 @@ public function __isset($name): bool $instance = $this; if ([$class] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name); + $scope = Registry::getScopeForRead($propertyScopes, $class, $name); if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) { if ($state = $this->lazyObjectState ?? null) { @@ -244,10 +250,10 @@ public function __unset($name): void $scope = null; $instance = $this; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2); - if ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { + if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { if ($state = $this->lazyObjectState ?? null) { $instance = $state->realInstance ??= ($state->initializer)(); } diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index ca6419efd56f7..e4fa760abe74f 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -33,7 +33,7 @@ public static function generateLazyGhost(\ReflectionClass $class): string if ($class->isFinal()) { throw new LogicException(\sprintf('Cannot generate lazy ghost: class "%s" is final.', $class->name)); } - if ($class->isInterface() || $class->isAbstract()) { + if ($class->isInterface() || $class->isAbstract() || $class->isTrait()) { throw new LogicException(\sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name)); } if (\stdClass::class !== $class->name && $class->isInternal()) { @@ -61,26 +61,34 @@ public static function generateLazyGhost(\ReflectionClass $class): string $hooks = ''; $propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name); - foreach ($propertyScopes as $name => $scope) { - if (!isset($scope[4]) || ($p = $scope[3])->isVirtual()) { + foreach ($propertyScopes as $key => [$scope, $name, , $access]) { + $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; + $flags = $access >> 2; + + if ($k !== $key || !($access & Hydrator::PROPERTY_HAS_HOOKS) || $flags & \ReflectionProperty::IS_VIRTUAL) { continue; } + if ($flags & (\ReflectionProperty::IS_FINAL | \ReflectionProperty::IS_PRIVATE)) { + throw new LogicException(sprintf('Cannot generate lazy ghost: property "%s::$%s" is final or private(set).', $class->name, $name)); + } + + $p = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name); + $type = self::exportType($p); - $hooks .= "\n public {$type} \${$name} {\n"; + $hooks .= "\n " + .($p->isProtected() ? 'protected' : 'public') + .($p->isProtectedSet() ? ' protected(set)' : '') + ." {$type} \${$name} {\n"; foreach ($p->getHooks() as $hook => $method) { - if ($method->isFinal()) { - throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is final.', $class->name, $method->name)); - } - if ('get' === $hook) { $ref = ($method->returnsReference() ? '&' : ''); - $hooks .= " {$ref}get { \$this->initializeLazyObject(); return parent::\${$name}::get(); }\n"; + $hooks .= " {$ref}get { \$this->initializeLazyObject(); return parent::\${$name}::get(); }\n"; } elseif ('set' === $hook) { $parameters = self::exportParameters($method, true); $arg = '$'.$method->getParameters()[0]->name; - $hooks .= " set({$parameters}) { \$this->initializeLazyObject(); parent::\${$name}::set({$arg}); }\n"; + $hooks .= " set({$parameters}) { \$this->initializeLazyObject(); parent::\${$name}::set({$arg}); }\n"; } else { throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is not supported.', $class->name, $method->name)); } @@ -89,7 +97,7 @@ public static function generateLazyGhost(\ReflectionClass $class): string $hooks .= " }\n"; } - $propertyScopes = self::exportPropertyScopes($class->name); + $propertyScopes = self::exportPropertyScopes($class->name, $propertyScopes); return <<name} implements \Symfony\Component\VarExporter\LazyObjectInterface @@ -126,13 +134,34 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf throw new LogicException(\sprintf('Cannot generate lazy proxy with PHP < 8.3: class "%s" is readonly.', $class->name)); } + $propertyScopes = $class ? Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name) : []; + $abstractProperties = []; $hookedProperties = []; if (\PHP_VERSION_ID >= 80400 && $class) { - $propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name); - foreach ($propertyScopes as $name => $scope) { - if (isset($scope[4]) && !($p = $scope[3])->isVirtual()) { - $hookedProperties[$name] = [$p, $p->getHooks()]; + foreach ($propertyScopes as $key => [$scope, $name, , $access]) { + $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; + $flags = $access >> 2; + + if ($k !== $key) { + continue; } + + if ($flags & \ReflectionProperty::IS_ABSTRACT) { + $abstractProperties[$name] = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name); + continue; + } + $abstractProperties[$name] = false; + + if (!($access & Hydrator::PROPERTY_HAS_HOOKS) || $flags & \ReflectionProperty::IS_VIRTUAL) { + continue; + } + + if ($flags & (\ReflectionProperty::IS_FINAL | \ReflectionProperty::IS_PRIVATE)) { + throw new LogicException(sprintf('Cannot generate lazy proxy: property "%s::$%s" is final or private(set).', $class->name, $name)); + } + + $p = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name); + $hookedProperties[$name] = [$p, $p->getHooks()]; } } @@ -143,8 +172,9 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf } $methodReflectors[] = $interface->getMethods(); - if (\PHP_VERSION_ID >= 80400 && !$class) { + if (\PHP_VERSION_ID >= 80400) { foreach ($interface->getProperties() as $p) { + $abstractProperties[$p->name] ??= $p; $hookedProperties[$p->name] ??= [$p, []]; $hookedProperties[$p->name][1] += $p->getHooks(); } @@ -152,47 +182,55 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf } $hooks = ''; + + foreach (array_filter($abstractProperties) as $name => $p) { + $type = self::exportType($p); + $hooks .= "\n " + .($p->isProtected() ? 'protected' : 'public') + .($p->isProtectedSet() ? ' protected(set)' : '') + ." {$type} \${$name};\n"; + } + foreach ($hookedProperties as $name => [$p, $methods]) { $type = self::exportType($p); - $hooks .= "\n public {$type} \${$p->name} {\n"; + $hooks .= "\n " + .($p->isProtected() ? 'protected' : 'public') + .($p->isProtectedSet() ? ' protected(set)' : '') + ." {$type} \${$name} {\n"; foreach ($methods as $hook => $method) { - if ($method->isFinal()) { - throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is final.', $class->name, $method->name)); - } - if ('get' === $hook) { $ref = ($method->returnsReference() ? '&' : ''); $hooks .= <<lazyObjectState)) { - return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$p->name}; - } - - return parent::\${$p->name}::get(); + {$ref}get { + if (isset(\$this->lazyObjectState)) { + return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$p->name}; } + return parent::\${$p->name}::get(); + } + EOPHP; } elseif ('set' === $hook) { $parameters = self::exportParameters($method, true); $arg = '$'.$method->getParameters()[0]->name; $hooks .= <<lazyObjectState)) { - \$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)(); - \$this->lazyObjectState->realInstance->{$p->name} = {$arg}; - } - - parent::\${$p->name}::set({$arg}); + set({$parameters}) { + if (isset(\$this->lazyObjectState)) { + \$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)(); + \$this->lazyObjectState->realInstance->{$p->name} = {$arg}; } + parent::\${$p->name}::set({$arg}); + } + EOPHP; } else { throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is not supported.', $class->name, $method->name)); } } - $hooks .= " }\n"; + $hooks .= " }\n"; } $extendsInternalClass = false; @@ -287,7 +325,7 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf $methods = ['initializeLazyObject' => implode('', $body).' }'] + $methods; } $body = $methods ? "\n".implode("\n\n", $methods)."\n" : ''; - $propertyScopes = $class ? self::exportPropertyScopes($class->name) : '[]'; + $propertyScopes = $class ? self::exportPropertyScopes($class->name, $propertyScopes) : '[]'; if ( $class?->hasMethod('__unserialize') @@ -444,12 +482,11 @@ public static function exportType(\ReflectionFunctionAbstract|\ReflectionPropert return implode($glue, $types); } - private static function exportPropertyScopes(string $parent): string + private static function exportPropertyScopes(string $parent, array $propertyScopes): string { - $propertyScopes = Hydrator::$propertyScopes[$parent] ??= Hydrator::getPropertyScopes($parent); uksort($propertyScopes, 'strnatcmp'); foreach ($propertyScopes as $k => $v) { - unset($propertyScopes[$k][3]); + unset($propertyScopes[$k][4]); } $propertyScopes = VarExporter::export($propertyScopes); $propertyScopes = str_replace(VarExporter::export($parent), 'parent::class', $propertyScopes); diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/BackedProperty.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/BackedProperty.php new file mode 100644 index 0000000000000..5c5d7688f97ca --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/BackedProperty.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures; + +class BackedProperty +{ + public private(set) string $name { + get => $this->name; + set => $value; + } + public function __construct(string $name) + { + $this->name = $name; + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php index ca6b235eba66d..6cac9ffc03d01 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php @@ -18,14 +18,5 @@ class ChildMagicClass extends MagicClass implements LazyObjectInterface { use LazyGhostTrait; - private const LAZY_OBJECT_PROPERTY_SCOPES = [ - "\0".self::class."\0".'data' => [self::class, 'data', null], - "\0".self::class."\0".'lazyObjectState' => [self::class, 'lazyObjectState', null], - "\0".parent::class."\0".'data' => [parent::class, 'data', null], - 'cloneCounter' => [self::class, 'cloneCounter', null], - 'data' => [self::class, 'data', null], - 'lazyObjectState' => [self::class, 'lazyObjectState', null], - ]; - private int $data = 123; } diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AbstractHooked.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AbstractHooked.php new file mode 100644 index 0000000000000..3d9cde20c7b15 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AbstractHooked.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; + +abstract class AbstractHooked implements HookedInterface +{ + abstract public string $bar { get; } + + public int $backed { + get { return $this->backed ??= 234; } + set { $this->backed = $value; } + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php new file mode 100644 index 0000000000000..a912ca403ca26 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; + +class AsymmetricVisibility +{ + public function __construct( + public private(set) int $foo, + private readonly int $bar, + ) { + } + + public function getBar(): int + { + return $this->bar; + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/Hooked.php similarity index 88% rename from src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php rename to src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/Hooked.php index 0c46d37afe922..62174f92d5847 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/Hooked.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\VarExporter\Tests\Fixtures; +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; class Hooked { diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedInterface.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedInterface.php new file mode 100644 index 0000000000000..9cdafd9c1fdfa --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedInterface.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; + +interface HookedInterface +{ + public string $foo { get; } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/backed-property.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/backed-property.php new file mode 100644 index 0000000000000..bcbc5729e9e5b --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/backed-property.php @@ -0,0 +1,17 @@ + [ + 'name' => [ + 'name', + ], + ], + ], + $o[0], + [] +); diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index 497eea9cb1989..af2b08ffadae8 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -16,7 +16,6 @@ use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\VarExporter\ProxyHelper; -use Symfony\Component\VarExporter\Tests\Fixtures\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildMagicClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildStdClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildTestClass; @@ -25,6 +24,8 @@ use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\MagicClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ReadOnlyClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\TestClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AsymmetricVisibility; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\SimpleObject; class LazyGhostTraitTest extends TestCase @@ -317,6 +318,26 @@ public function testPropertyHooks() $this->assertSame(345, $object->backed); } + /** + * @requires PHP 8.4 + */ + public function testAsymmetricVisibility() + { + $object = $this->createLazyGhost(AsymmetricVisibility::class, function ($instance) { + $instance->__construct(123, 234); + }); + + $this->assertSame(123, $object->foo); + $this->assertSame(234, $object->getBar()); + + $object = $this->createLazyGhost(AsymmetricVisibility::class, function ($instance) { + $instance->__construct(123, 234); + }); + + $this->assertSame(234, $object->getBar()); + $this->assertSame(123, $object->foo); + } + /** * @template T * diff --git a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php index 72f34719e9041..cbd8d8279101f 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php @@ -18,8 +18,10 @@ use Symfony\Component\VarExporter\Exception\LogicException; use Symfony\Component\VarExporter\LazyProxyTrait; use Symfony\Component\VarExporter\ProxyHelper; -use Symfony\Component\VarExporter\Tests\Fixtures\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\RegularClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AbstractHooked; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AsymmetricVisibility; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\FinalPublicClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\ReadOnlyClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass; @@ -325,11 +327,12 @@ public function testReinitReadonlyLazyProxy() /** * @requires PHP 8.4 */ - public function testPropertyHooks() + public function testConcretePropertyHooks() { $initialized = false; $object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) { $initialized = true; + return new Hooked(); }); @@ -341,6 +344,7 @@ public function testPropertyHooks() $initialized = false; $object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) { $initialized = true; + return new Hooked(); }); @@ -349,6 +353,60 @@ public function testPropertyHooks() $this->assertSame(345, $object->backed); } + /** + * @requires PHP 8.4 + */ + public function testAbstractPropertyHooks() + { + $initialized = false; + $object = $this->createLazyProxy(AbstractHooked::class, function () use (&$initialized) { + $initialized = true; + + return new class extends AbstractHooked { + public string $foo = 'Foo'; + public string $bar = 'Bar'; + }; + }); + + $this->assertSame('Foo', $object->foo); + $this->assertSame('Bar', $object->bar); + $this->assertTrue($initialized); + + $initialized = false; + $object = $this->createLazyProxy(AbstractHooked::class, function () use (&$initialized) { + $initialized = true; + + return new class extends AbstractHooked { + public string $foo = 'Foo'; + public string $bar = 'Bar'; + }; + }); + + $this->assertSame('Bar', $object->bar); + $this->assertSame('Foo', $object->foo); + $this->assertTrue($initialized); + } + + /** + * @requires PHP 8.4 + */ + public function testAsymmetricVisibility() + { + $object = $this->createLazyProxy(AsymmetricVisibility::class, function () { + return new AsymmetricVisibility(123, 234); + }); + + $this->assertSame(123, $object->foo); + $this->assertSame(234, $object->getBar()); + + $object = $this->createLazyProxy(AsymmetricVisibility::class, function () { + return new AsymmetricVisibility(123, 234); + }); + + $this->assertSame(234, $object->getBar()); + $this->assertSame(123, $object->foo); + } + /** * @template T * diff --git a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php index 81cf70ce04271..b9c67adf17437 100644 --- a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php +++ b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\VarExporter\Exception\LogicException; use Symfony\Component\VarExporter\ProxyHelper; -use Symfony\Component\VarExporter\Tests\Fixtures\Hooked; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Php82NullStandaloneReturnType; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass; @@ -253,10 +253,9 @@ public function testNullStandaloneReturnType() */ public function testPropertyHooks() { - self::assertStringContainsString( - "[parent::class, 'backed', null, 4 => true]", - ProxyHelper::generateLazyProxy(new \ReflectionClass(Hooked::class)) - ); + $proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(Hooked::class)); + self::assertStringContainsString("'backed' => [parent::class, 'backed', null, 7],", $proxyCode); + self::assertStringContainsString("'notBacked' => [parent::class, 'notBacked', null, 2055],", $proxyCode); } } diff --git a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php index 10942a56caaca..6ca98b91487aa 100644 --- a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php +++ b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php @@ -16,6 +16,7 @@ use Symfony\Component\VarExporter\Exception\ClassNotFoundException; use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; use Symfony\Component\VarExporter\Internal\Registry; +use Symfony\Component\VarExporter\Tests\Fixtures\BackedProperty; use Symfony\Component\VarExporter\Tests\Fixtures\FooReadonly; use Symfony\Component\VarExporter\Tests\Fixtures\FooSerializable; use Symfony\Component\VarExporter\Tests\Fixtures\FooUnitEnum; @@ -235,6 +236,12 @@ public static function provideExport() yield ['unit-enum', [FooUnitEnum::Bar], true]; yield ['readonly', new FooReadonly('k', 'v')]; + + if (\PHP_VERSION_ID < 80400) { + return; + } + + yield ['backed-property', new BackedProperty('name')]; } public function testUnicodeDirectionality() diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 266b89e821a9e..f759a64c1b2fb 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -1165,7 +1165,18 @@ private function lexInlineQuotedString(int &$cursor = 0): string private function lexUnquotedString(int &$cursor): string { $offset = $cursor; - $cursor += strcspn($this->currentLine, '[]{},:', $cursor); + + while ($cursor < strlen($this->currentLine)) { + if (in_array($this->currentLine[$cursor], ['[', ']', '{', '}', ',', ':'], true)) { + break; + } + + if (\in_array($this->currentLine[$cursor], [' ', "\t"], true) && '#' === ($this->currentLine[$cursor + 1] ?? '')) { + break; + } + + ++$cursor; + } if ($cursor === $offset) { throw new ParseException('Malformed unquoted YAML string.'); @@ -1242,7 +1253,7 @@ private function consumeWhitespaces(int &$cursor): bool $whitespacesConsumed = 0; do { - $whitespaceOnlyTokenLength = strspn($this->currentLine, ' ', $cursor); + $whitespaceOnlyTokenLength = strspn($this->currentLine, " \t", $cursor); $whitespacesConsumed += $whitespaceOnlyTokenLength; $cursor += $whitespaceOnlyTokenLength; diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 345c930dd1e22..cdd37ed4739f1 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -1769,6 +1769,69 @@ public function testParseMultiLineUnquotedString() $this->assertSame(['foo' => 'bar baz foobar foo', 'bar' => 'baz'], $this->parser->parse($yaml)); } + /** + * @dataProvider unquotedStringWithTrailingComment + */ + public function testParseMultiLineUnquotedStringWithTrailingComment(string $yaml, array $expected) + { + $this->assertSame($expected, $this->parser->parse($yaml)); + } + + public function unquotedStringWithTrailingComment() + { + return [ + 'comment after comma' => [ + <<<'YAML' + { + foo: 3, # comment + bar: 3 + } + YAML, + ['foo' => 3, 'bar' => 3], + ], + 'comment after space' => [ + <<<'YAML' + { + foo: 3 # comment + } + YAML, + ['foo' => 3], + ], + 'comment after space, but missing space after #' => [ + <<<'YAML' + { + foo: 3 #comment + } + YAML, + ['foo' => 3], + ], + 'comment after tab' => [ + << 3], + ], + 'comment after tab, but missing space after #' => [ + << 3], + ], + '# in mapping value' => [ + <<<'YAML' + { + foo: example.com/#about + } + YAML, + ['foo' => 'example.com/#about'], + ], + ]; + } + /** * @dataProvider escapedQuotationCharactersInQuotedStrings */ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy