diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ff72a1cc13a5c..be833bfec1a14 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 6.4 for features / 5.4 or 6.3 for bug fixes +| Branch? | 7.1 for features / 5.4, 6.3, 6.4, or 7.0 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8928a32f75ea3..4210efac57606 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -48,6 +48,12 @@ jobs: image: redis:6.2.8 ports: - 16379:6379 + redis-authenticated: + image: redis:6.2.8 + ports: + - 16380:6379 + env: + REDIS_ARGS: "--requirepass p@ssword" redis-cluster: image: grokzen/redis-cluster:6.2.8 ports: @@ -171,6 +177,7 @@ jobs: run: ./phpunit --group integration -v env: REDIS_HOST: 'localhost:16379' + REDIS_AUTHENTICATED_HOST: 'localhost:16380' REDIS_CLUSTER_HOSTS: 'localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005' REDIS_SENTINEL_HOSTS: 'localhost:26379 localhost:26379 localhost:26379' REDIS_SENTINEL_SERVICE: redis_sentinel diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index b66e1b53b60f4..0753dc03e2789 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -6,7 +6,7 @@ on: schedule: - cron: '34 4 * * 6' push: - branches: [ "6.4" ] + branches: [ "7.1" ] # Declare default permissions as read only. permissions: read-all diff --git a/CHANGELOG-6.3.md b/CHANGELOG-6.3.md index d50e65ef16b3f..a7490b8179172 100644 --- a/CHANGELOG-6.3.md +++ b/CHANGELOG-6.3.md @@ -7,6 +7,38 @@ in 6.3 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v6.3.0...v6.3.1 +* 6.3.9 (2023-11-29) + + * bug #52786 [Serializer] Revert allowed attributes fix (mtarld) + * bug #52780 [DependencyInjection] don't check parameter values if they are not set (xabbuh) + * bug #52762 [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior (nicolas-grekas) + * bug #52759 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy (nicolas-grekas) + * bug #52767 [Serializer] Fix normalization relying on allowed attributes only (mtarld) + * bug #52727 [String] Fix Inflector for 'icon' (podhy) + * bug #52677 [Translation] [Lokalise] Fix language format on Lokalise Provider (welcoMattic) + * bug #52715 [Cache] fix detecting the database server version (xabbuh) + * bug #52688 [Cache] Add url decoding of password in `RedisTrait` DSN (alexandre-daubois) + * bug #52172 [Serializer] Fix denormalizing empty string into `object|null` parameter (Jeroeny) + * bug #52693 [Messenger] Fix message handlers with multiple `from_transports` (valtzu) + * bug #52684 [PropertyInfo] Fixed promoted property type detection for `PhpStanExtractor` (LastDragon-ru) + * bug #52681 [Serializer] Fix support for DiscriminatorMap in PropertyNormalizer (mtarld) + * bug #52680 [Serializer] Fix access to private properties/getters when using the ``@Ignore`` annotation (mtarld) + * bug #52713 [Serializer] Fix deserialization_path missing using contructor (mtarld) + * bug #52683 [Serializer] Fix constructor deserialization path (mtarld) + * bug #52707 [HttpKernel] Fix logging deprecations to the "php" channel when channel "deprecation" is not defined (nicolas-grekas) + * bug #52589 [Serializer] Fix XML attributes not added on empty node (mtarld) + * bug #52686 [Cache] fix detecting the server version with Doctrine DBAL 4 (xabbuh) + * bug #52629 [Messenger] Fix support for Redis Sentinel using php-redis 6.0.0 (pepeh) + * bug #52459 [Cache][HttpFoundation][Lock] Fix PDO store not creating table + add tests (HypeMC) + * bug #52626 [Serializer] Fix denormalizing date intervals having both weeks and days (oneNevan) + * bug #52578 [Serializer] Fix denormalize constructor arguments (mtarld) + * bug #52526 Add some more non-countable English nouns (paullallier) + * bug #52631 [DomCrawler] Revert "bug #52579 UriResolver support path with colons" (lyrixx) + * bug #52618 [VarExporter] Fix handling mangled property names returned by __sleep() (nicolas-grekas) + * bug #52588 [Messenger] Use extension_loaded call to check if pcntl extension is loaded, as SIGTERM might be set be swoole (Sergii Dolgushev) + * bug #52579 [DomCrawler] UriResolver support path with colons (vdauchy) + * bug #52581 [Messenger] attach all required parameters to query (xabbuh) + * 6.3.8 (2023-11-10) * bug #51666 [RateLimiter] CompoundLimiter was accepting requests even when some limiters already consumed all tokens (10n) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4e0d9c4150104..b7f58c00c8501 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -15,39 +15,39 @@ The Symfony Connect username in parenthesis allows to get more information - Thomas Calvet (fancyweb) - Christophe Coevoet (stof) - Jordi Boggiano (seldaek) - - Maxime Steinhausser (ogizanagi) - Wouter de Jong (wouterj) + - Maxime Steinhausser (ogizanagi) - Kévin Dunglas (dunglas) - Victor Berchet (victor) - Ryan Weaver (weaverryan) - Jérémy DERUSSÉ (jderusse) - - Roland Franssen - Javier Eguiluz (javier.eguiluz) + - Roland Franssen - Johannes S (johannes) - Kris Wallsmith (kriswallsmith) - Jakub Zalas (jakubzalas) - - Yonel Ceruto (yonelceruto) + - Alexandre Daubois (alexandre-daubois) - Jules Pietri (heah) - Oskar Stark (oskarstark) - - Tobias Nyholm (tobias) + - Yonel Ceruto (yonelceruto) - Hugo Hamon (hhamon) - - Alexandre Daubois (alexandre-daubois) + - Tobias Nyholm (tobias) - Samuel ROZE (sroze) - Pascal Borreli (pborreli) - Romain Neutron - Joseph Bielawski (stloyd) - Drak (drak) - Abdellatif Ait boudad (aitboudad) + - Jérôme Tamarelle (gromnan) - Lukas Kahwe Smith (lsmith) + - Antoine Lamirault (alamirault) - Hamza Amrouche (simperfit) - - Martin Hasoň (hason) - Kevin Bond (kbond) - - Jérôme Tamarelle (gromnan) + - Martin Hasoň (hason) + - HypeMC (hypemc) - Jeremy Mikola (jmikola) - - Antoine Lamirault (alamirault) - Jean-François Simon (jfsimon) - Benjamin Eberlei (beberlei) - - HypeMC (hypemc) - Igor Wiedler - Jan Schädlich (jschaedl) - Mathieu Lechat (mat_the_cat) @@ -76,28 +76,30 @@ The Symfony Connect username in parenthesis allows to get more information - Mathieu Piot (mpiot) - Alexander Schranz (alexander-schranz) - Vasilij Duško (staff) + - Vincent Langlet (deviling) - Sarah Khalil (saro0h) - Laurent VOULLEMIER (lvo) - Konstantin Kudryashov (everzet) - - Vincent Langlet (deviling) - Guilhem N (guilhemn) - Bilal Amarni (bamarni) - Eriksen Costa + - Gary PEGEOT (gary-p) - Mathieu Santostefano (welcomattic) - Florin Patan (florinpatan) - Vladimir Reznichenko (kalessil) - Peter Rehm (rpet) - Henrik Bjørnskov (henrikbjorn) + - Allison Guilhem (a_guilhem) - Andrej Hudec (pulzarraider) - Jáchym Toušek (enumag) - David Buchmann (dbu) + - Dariusz Ruminski - Christian Raue - Eric Clemmons (ericclemmons) - Denis (yethee) - Michel Weimerskirch (mweimerskirch) - Issei Murasawa (issei_m) - Douglas Greenshields (shieldo) - - Gary PEGEOT (gary-p) - Alex Pott - Fran Moreno (franmomu) - Arnout Boks (aboks) @@ -105,9 +107,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ruud Kamphuis (ruudk) - Henrik Westphal (snc) - Dariusz Górecki (canni) - - Allison Guilhem (a_guilhem) - Ener-Getick - - Dariusz Ruminski - Graham Campbell (graham) - Tugdual Saunier (tucksaun) - Lee McDermott @@ -134,7 +134,9 @@ The Symfony Connect username in parenthesis allows to get more information - Joel Wurtz (brouznouf) - Sebastiaan Stok (sstok) - Maxime STEINHAUSSER + - Frank A. Fiebig (fafiebig) - gnito-org + - Baldini - Tim Nagel (merk) - Chris Wilkinson (thewilkybarkid) - Jérôme Vasseur (jvasseur) @@ -178,8 +180,6 @@ The Symfony Connect username in parenthesis allows to get more information - Ion Bazan (ionbazan) - OGAWA Katsuhiro (fivestar) - Jhonny Lidfors (jhonne) - - Frank A. Fiebig (fafiebig) - - Baldini - Juti Noppornpitak (shiroyuki) - Gregor Harlan (gharlan) - Michael Babker (mbabker) @@ -237,6 +237,7 @@ The Symfony Connect username in parenthesis allows to get more information - Alessandro Lai (jean85) - 77web - Gocha Ossinkine (ossinkine) + - Martin Auswöger - Jesse Rushlow (geeshoe) - Matthieu Ouellette-Vachon (maoueh) - Michał Pipa (michal.pipa) @@ -251,12 +252,14 @@ The Symfony Connect username in parenthesis allows to get more information - Roland Franssen :) - GDIBass - Samuel NELA (snela) + - Tac Tacelosky (tacman1123) - Vincent AUBERT (vincent) - Fabien Bourigault (fbourigault) - Michael Voříšek - zairig imad (zairigimad) - Colin O'Dell (colinodell) - Sébastien Alfaiate (seb33300) + - Valtteri R (valtzu) - James Halsall (jaitsu) - Christian Scheb - Guillaume (guill) @@ -298,7 +301,6 @@ The Symfony Connect username in parenthesis allows to get more information - Andreas Hucks (meandmymonkey) - Jan Rosier (rosier) - Noel Guilbert (noel) - - Martin Auswöger - Stadly - Stepan Anchugov (kix) - bronze1man @@ -329,7 +331,6 @@ The Symfony Connect username in parenthesis allows to get more information - François Zaninotto (fzaninotto) - Dustin Whittle (dustinwhittle) - Timothée Barray (tyx) - - Valtteri R (valtzu) - jeff - Bob van de Vijver (bobvandevijver) - John Kary (johnkary) @@ -340,6 +341,7 @@ The Symfony Connect username in parenthesis allows to get more information - Marcin Sikoń (marphi) - Michele Orselli (orso) - Sven Paulus (subsven) + - Tomasz Kowalczyk (thunderer) - Daniel Burger - Maxime Veber (nek-) - Bastien Jaillot (bastnic) @@ -448,7 +450,6 @@ The Symfony Connect username in parenthesis allows to get more information - Wouter Van Hecke - Baptiste Lafontaine (magnetik) - Iker Ibarguren (ikerib) - - Tomasz Kowalczyk (thunderer) - Indra Gunawan (indragunawan) - Michael Holm (hollo) - Arjen van der Meijden @@ -537,6 +538,7 @@ The Symfony Connect username in parenthesis allows to get more information - Artur Eshenbrener - Harm van Tilborg (hvt) - Thomas Perez (scullwm) + - Cédric Anne - smoench - Felix Labrecque - mondrake (mondrake) @@ -577,6 +579,7 @@ The Symfony Connect username in parenthesis allows to get more information - SiD (plbsid) - Greg Thornton (xdissent) - Alex Bowers + - Michel Roca (mroca) - Fabien S (bafs) - Costin Bereveanu (schniper) - Andrii Dembitskyi @@ -618,6 +621,7 @@ The Symfony Connect username in parenthesis allows to get more information - Oscar Cubo Medina (ocubom) - Karel Souffriau - Christophe L. (christophelau) + - a.dmitryuk - Anthon Pang (robocoder) - Julien Galenski (ruian) - Ben Scott (bpscott) @@ -685,7 +689,6 @@ The Symfony Connect username in parenthesis allows to get more information - Dries Vints - Judicaël RUFFIEUX (axanagor) - Chris Sedlmayr (catchamonkey) - - Cédric Anne - DerManoMann - Jérôme Tanghe (deuchnord) - Mathias STRASSER (roukmoute) @@ -735,6 +738,7 @@ The Symfony Connect username in parenthesis allows to get more information - Axel Guckelsberger (guite) - Sam Fleming (sam_fleming) - Alex Bakhturin + - Belhassen Bouchoucha (crownbackend) - Patrick Reimers (preimers) - Brayden Williams (redstar504) - insekticid @@ -860,7 +864,6 @@ The Symfony Connect username in parenthesis allows to get more information - Ilija Tovilo (ilijatovilo) - Sander Toonen (xatoo) - Zach Badgett (zachbadgett) - - a.dmitryuk - Loïc Faugeron - Aurélien Fredouelle - Pavel Campr (pcampr) @@ -872,7 +875,6 @@ The Symfony Connect username in parenthesis allows to get more information - Benjamin Morel - Guilherme Ferreira - Geoffrey Tran (geoff) - - Tac Tacelosky (tacman1123) - Jannik Zschiesche - Bernd Stellwag - Jan Ole Behrens (deegital) @@ -1050,6 +1052,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ruben Jacobs (rubenj) - Simon Schick (simonsimcity) - Tristan Roussel + - NickSdot - Niklas Keller - Alexandre parent - Cameron Porter @@ -1094,7 +1097,6 @@ The Symfony Connect username in parenthesis allows to get more information - Raphaëll Roussel - Michael Lutz - jochenvdv - - Michel Roca (mroca) - Reedy - Arturas Smorgun (asarturas) - Aleksandr Volochnev (exelenz) @@ -1141,7 +1143,6 @@ The Symfony Connect username in parenthesis allows to get more information - kylekatarnls (kylekatarnls) - Steve Grunwell - Yuen-Chi Lian - - Belhassen Bouchoucha (crownbackend) - Mathias Brodala (mbrodala) - Robert Fischer (sandoba) - Tarjei Huse (tarjei) @@ -1222,6 +1223,7 @@ The Symfony Connect username in parenthesis allows to get more information - Mike Meier (mykon) - Pedro Miguel Maymone de Resende (pedroresende) - stlrnz + - javaDeveloperKid - Masterklavi - Adrien Wilmet (adrienfr) - Franco Traversaro (belinde) @@ -1361,6 +1363,7 @@ The Symfony Connect username in parenthesis allows to get more information - Simon Heimberg (simon_heimberg) - Morten Wulff (wulff) - Don Pinkster + - Jonas Elfering - Maksim Muruev - Emil Einarsson - 243083df @@ -1390,6 +1393,7 @@ The Symfony Connect username in parenthesis allows to get more information - Markus S. (staabm) - Marc Laporte - Michał Jusięga + - Dominik Ulrich - den - Gábor Tóth - ouardisoft @@ -1668,6 +1672,7 @@ The Symfony Connect username in parenthesis allows to get more information - Vedran Mihočinec (v-m-i) - Sergey Novikov (s12v) - creiner + - Jan Pintr - ProgMiner - Marcos Quesada (marcos_quesada) - Matthew (mattvick) @@ -1714,6 +1719,7 @@ 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) - Martynas Narbutas @@ -2200,6 +2206,7 @@ The Symfony Connect username in parenthesis allows to get more information - Evan C - BrokenSourceCode - Fabian Haase + - roog - parinz1234 - Romain Geissler - Adrien Moiruad @@ -2269,7 +2276,6 @@ The Symfony Connect username in parenthesis allows to get more information - Thomas Counsell - BilgeXA - mmokhi - - javaDeveloperKid - Serhii Smirnov - Robert Queck - Peter Bouwdewijn @@ -2488,6 +2494,7 @@ The Symfony Connect username in parenthesis allows to get more information - AntoineDly - Konstantinos Alexiou - Andrii Boiko + - louismariegaborit - Dilek Erkut - Harold Iedema - WaiSkats diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php index b4649182b18f7..63a760fdb19b9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php @@ -27,7 +27,7 @@ param('debug.error_handler.throw_at'), param('kernel.debug'), param('kernel.debug'), - service('logger')->nullOnInvalid(), + null, // Deprecation logger if different from the one above ]) ->tag('monolog.logger', ['channel' => 'php']) diff --git a/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php b/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php index 3843af125c1b1..3fedb1edfb51e 100644 --- a/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php @@ -21,6 +21,7 @@ use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\ServerVersionProvider; use Doctrine\DBAL\Tools\DsnParser; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\Marshaller\DefaultMarshaller; @@ -388,12 +389,14 @@ private function getServerVersion(): string return $this->serverVersion; } - $conn = $this->conn->getWrappedConnection(); - if ($conn instanceof ServerInfoAwareConnection) { - return $this->serverVersion = $conn->getServerVersion(); + if ($this->conn instanceof ServerVersionProvider || $this->conn instanceof ServerInfoAwareConnection) { + return $this->serverVersion = $this->conn->getServerVersion(); } - return $this->serverVersion = '0'; + // The condition should be removed once support for DBAL <3.3 is dropped + $conn = method_exists($this->conn, 'getNativeConnection') ? $this->conn->getNativeConnection() : $this->conn->getWrappedConnection(); + + return $this->serverVersion = $conn->getAttribute(\PDO::ATTR_SERVER_VERSION); } private function addTableToSchema(Schema $schema): void diff --git a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php index 7e7f51f912439..cf0059e840f92 100644 --- a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php @@ -285,8 +285,8 @@ protected function doSave(array $values, int $lifetime): array|bool $lifetime = $lifetime ?: null; try { $stmt = $conn->prepare($sql); - } catch (\PDOException) { - if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + } catch (\PDOException $e) { + if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) { $this->createTable(); } $stmt = $conn->prepare($sql); @@ -320,8 +320,8 @@ protected function doSave(array $values, int $lifetime): array|bool foreach ($values as $id => $data) { try { $stmt->execute(); - } catch (\PDOException) { - if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + } catch (\PDOException $e) { + if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) { $this->createTable(); } $stmt->execute(); @@ -369,4 +369,21 @@ private function getServerVersion(): string { return $this->serverVersion ??= $this->conn->getAttribute(\PDO::ATTR_SERVER_VERSION); } + + private function isTableMissing(\PDOException $exception): bool + { + $driver = $this->driver; + $code = $exception->getCode(); + + switch (true) { + case 'pgsql' === $driver && '42P01' === $code: + case 'sqlite' === $driver && str_contains($exception->getMessage(), 'no such table:'): + case 'oci' === $driver && 942 === $code: + case 'sqlsrv' === $driver && 208 === $code: + case 'mysql' === $driver && 1146 === $code: + return true; + default: + return false; + } + } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php index 7f250f7b53596..179e4de85b9fb 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php @@ -18,12 +18,13 @@ use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; use Doctrine\DBAL\Schema\Schema; -use PHPUnit\Framework\SkippedTestSuiteError; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter; use Symfony\Component\Cache\Tests\Fixtures\DriverWrapper; /** + * @requires extension pdo_sqlite + * * @group time-sensitive */ class DoctrineDbalAdapterTest extends AdapterTestCase @@ -32,10 +33,6 @@ class DoctrineDbalAdapterTest extends AdapterTestCase public static function setUpBeforeClass(): void { - if (!\extension_loaded('pdo_sqlite')) { - throw new SkippedTestSuiteError('Extension pdo_sqlite required.'); - } - self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); } @@ -107,13 +104,12 @@ public function testConfigureSchemaTableExists() } /** - * @dataProvider provideDsn + * @dataProvider provideDsnWithSQLite */ - public function testDsn(string $dsn, string $file = null) + public function testDsnWithSQLite(string $dsn, string $file = null) { try { $pool = new DoctrineDbalAdapter($dsn); - $pool->createTable(); $item = $pool->getItem('key'); $item->set('value'); @@ -125,12 +121,35 @@ public function testDsn(string $dsn, string $file = null) } } - public static function provideDsn() + public static function provideDsnWithSQLite() { $dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); - yield ['sqlite://localhost/'.$dbFile.'1', $dbFile.'1']; - yield ['sqlite3:///'.$dbFile.'3', $dbFile.'3']; - yield ['sqlite://localhost/:memory:']; + yield 'SQLite file' => ['sqlite://localhost/'.$dbFile.'1', $dbFile.'1']; + yield 'SQLite3 file' => ['sqlite3:///'.$dbFile.'3', $dbFile.'3']; + yield 'SQLite in memory' => ['sqlite://localhost/:memory:']; + } + + /** + * @requires extension pdo_pgsql + * + * @group integration + */ + public function testDsnWithPostgreSQL() + { + if (!$host = getenv('POSTGRES_HOST')) { + $this->markTestSkipped('Missing POSTGRES_HOST env variable'); + } + + try { + $pool = new DoctrineDbalAdapter('pgsql://postgres:password@'.$host); + + $item = $pool->getItem('key'); + $item->set('value'); + $this->assertTrue($pool->save($item)); + } finally { + $pdo = new \PDO('pgsql:host='.$host.';user=postgres;password=password'); + $pdo->exec('DROP TABLE IF EXISTS cache_items'); + } } protected function isPruned(DoctrineDbalAdapter $cache, string $name): bool diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php index 4a8f233cec308..74e86263eebc2 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php @@ -11,11 +11,12 @@ namespace Symfony\Component\Cache\Tests\Adapter; -use PHPUnit\Framework\SkippedTestSuiteError; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\PdoAdapter; /** + * @requires extension pdo_sqlite + * * @group time-sensitive */ class PdoAdapterTest extends AdapterTestCase @@ -24,10 +25,6 @@ class PdoAdapterTest extends AdapterTestCase public static function setUpBeforeClass(): void { - if (!\extension_loaded('pdo_sqlite')) { - throw new SkippedTestSuiteError('Extension pdo_sqlite required.'); - } - self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); $pool = new PdoAdapter('sqlite:'.self::$dbFile); @@ -69,13 +66,12 @@ public function testCleanupExpiredItems() } /** - * @dataProvider provideDsn + * @dataProvider provideDsnSQLite */ - public function testDsn(string $dsn, string $file = null) + public function testDsnWithSQLite(string $dsn, string $file = null) { try { $pool = new PdoAdapter($dsn); - $pool->createTable(); $item = $pool->getItem('key'); $item->set('value'); @@ -87,11 +83,36 @@ public function testDsn(string $dsn, string $file = null) } } - public static function provideDsn() + public static function provideDsnSQLite() { $dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); - yield ['sqlite:'.$dbFile.'2', $dbFile.'2']; - yield ['sqlite::memory:']; + yield 'SQLite file' => ['sqlite:'.$dbFile.'2', $dbFile.'2']; + yield 'SQLite in memory' => ['sqlite::memory:']; + } + + /** + * @requires extension pdo_pgsql + * + * @group integration + */ + public function testDsnWithPostgreSQL() + { + if (!$host = getenv('POSTGRES_HOST')) { + $this->markTestSkipped('Missing POSTGRES_HOST env variable'); + } + + $dsn = 'pgsql:host='.$host.';user=postgres;password=password'; + + try { + $pool = new PdoAdapter($dsn); + + $item = $pool->getItem('key'); + $item->set('value'); + $this->assertTrue($pool->save($item)); + } finally { + $pdo = new \PDO($dsn); + $pdo->exec('DROP TABLE IF EXISTS cache_items'); + } } protected function isPruned(PdoAdapter $cache, string $name): bool diff --git a/src/Symfony/Component/Cache/Tests/Traits/RedisTraitTest.php b/src/Symfony/Component/Cache/Tests/Traits/RedisTraitTest.php index 5997968468276..e287491e74a61 100644 --- a/src/Symfony/Component/Cache/Tests/Traits/RedisTraitTest.php +++ b/src/Symfony/Component/Cache/Tests/Traits/RedisTraitTest.php @@ -15,15 +15,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Traits\RedisTrait; +/** + * @requires extension redis + */ class RedisTraitTest extends TestCase { - public static function setUpBeforeClass(): void - { - if (!getenv('REDIS_CLUSTER_HOSTS')) { - throw new SkippedTestSuiteError('REDIS_CLUSTER_HOSTS env var is not defined.'); - } - } - /** * @dataProvider provideCreateConnection */ @@ -42,6 +38,19 @@ public function testCreateConnection(string $dsn, string $expectedClass) self::assertInstanceOf($expectedClass, $connection); } + public function testUrlDecodeParameters() + { + if (!getenv('REDIS_AUTHENTICATED_HOST')) { + self::markTestSkipped('REDIS_AUTHENTICATED_HOST env var is not defined.'); + } + + $mock = self::getObjectForTrait(RedisTrait::class); + $connection = $mock::createConnection('redis://:p%40ssword@'.getenv('REDIS_AUTHENTICATED_HOST')); + + self::assertInstanceOf(\Redis::class, $connection); + self::assertSame('p@ssword', $connection->getAuth()); + } + public static function provideCreateConnection(): array { $hosts = array_map(fn ($host) => sprintf('host[%s]', $host), explode(' ', getenv('REDIS_CLUSTER_HOSTS'))); diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index f28d6ce3b7972..24997500d71d7 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -101,9 +101,9 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra $params = preg_replace_callback('#^'.$scheme.':(//)?(?:(?:(?[^:@]*+):)?(?[^@]*+)@)?#', function ($m) use (&$auth) { if (isset($m['password'])) { if (\in_array($m['user'], ['', 'default'], true)) { - $auth = $m['password']; + $auth = rawurldecode($m['password']); } else { - $auth = [$m['user'], $m['password']]; + $auth = [rawurldecode($m['user']), rawurldecode($m['password'])]; } if ('' === $auth) { diff --git a/src/Symfony/Component/Console/Event/ConsoleCommandEvent.php b/src/Symfony/Component/Console/Event/ConsoleCommandEvent.php index 31c9ee99a29f9..0757a23f6000f 100644 --- a/src/Symfony/Component/Console/Event/ConsoleCommandEvent.php +++ b/src/Symfony/Component/Console/Event/ConsoleCommandEvent.php @@ -12,7 +12,10 @@ namespace Symfony\Component\Console\Event; /** - * Allows to do things before the command is executed, like skipping the command or changing the input. + * Allows to do things before the command is executed, like skipping the command or executing code before the command is + * going to be executed. + * + * Changing the input arguments will have no effect. * * @author Fabien Potencier */ diff --git a/src/Symfony/Component/DomCrawler/Tests/UriResolverTest.php b/src/Symfony/Component/DomCrawler/Tests/UriResolverTest.php index b0c227abf5478..ab98cb52cbeeb 100644 --- a/src/Symfony/Component/DomCrawler/Tests/UriResolverTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/UriResolverTest.php @@ -84,6 +84,8 @@ public static function provideResolverTests() ['foo', 'http://localhost?bar=1', 'http://localhost/foo'], ['foo', 'http://localhost#bar', 'http://localhost/foo'], + + ['http://', 'http://localhost', 'http://'], ]; } } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php index aac62296ef5c7..ff5b70d8173b2 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -78,6 +78,7 @@ public static function createHandler(object|string $connection, array $options = } $connection = DriverManager::getConnection($params, $config); + // The condition should be removed once support for DBAL <3.3 is dropped $connection = method_exists($connection, 'getNativeConnection') ? $connection->getNativeConnection() : $connection->getWrappedConnection(); // no break; diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index ae1343252eef6..41f48a3a56d29 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -76,11 +76,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.3.8'; - public const VERSION_ID = 60308; + public const VERSION = '6.3.9'; + public const VERSION_ID = 60309; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 3; - public const RELEASE_VERSION = 8; + public const RELEASE_VERSION = 9; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '01/2024'; diff --git a/src/Symfony/Component/Lock/Store/PdoStore.php b/src/Symfony/Component/Lock/Store/PdoStore.php index def487c988c58..5582dc0278145 100644 --- a/src/Symfony/Component/Lock/Store/PdoStore.php +++ b/src/Symfony/Component/Lock/Store/PdoStore.php @@ -94,8 +94,8 @@ public function save(Key $key) $conn = $this->getConnection(); try { $stmt = $conn->prepare($sql); - } catch (\PDOException) { - if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + } catch (\PDOException $e) { + if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) { $this->createTable(); } $stmt = $conn->prepare($sql); @@ -106,9 +106,19 @@ public function save(Key $key) try { $stmt->execute(); - } catch (\PDOException) { - // the lock is already acquired. It could be us. Let's try to put off. - $this->putOffExpiration($key, $this->initialTtl); + } catch (\PDOException $e) { + if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) { + $this->createTable(); + + try { + $stmt->execute(); + } catch (\PDOException $e) { + $this->putOffExpiration($key, $this->initialTtl); + } + } else { + // the lock is already acquired. It could be us. Let's try to put off. + $this->putOffExpiration($key, $this->initialTtl); + } } $this->randomlyPrune(); @@ -238,4 +248,21 @@ private function getCurrentTimestampStatement(): string default => (string) time(), }; } + + private function isTableMissing(\PDOException $exception): bool + { + $driver = $this->getDriver(); + $code = $exception->getCode(); + + switch (true) { + case 'pgsql' === $driver && '42P01' === $code: + case 'sqlite' === $driver && str_contains($exception->getMessage(), 'no such table:'): + case 'oci' === $driver && 942 === $code: + case 'sqlsrv' === $driver && 208 === $code: + case 'mysql' === $driver && 1146 === $code: + return true; + default: + return false; + } + } } diff --git a/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php index a66cd037d9ffc..4ef2e7d771b50 100644 --- a/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php @@ -74,9 +74,9 @@ public function testAbortAfterExpiration() } /** - * @dataProvider provideDsn + * @dataProvider provideDsnWithSQLite */ - public function testDsn(string $dsn, string $file = null) + public function testDsnWithSQLite(string $dsn, string $file = null) { $key = new Key(uniqid(__METHOD__, true)); @@ -92,12 +92,36 @@ public function testDsn(string $dsn, string $file = null) } } - public static function provideDsn() + public static function provideDsnWithSQLite() { $dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); - yield ['sqlite://localhost/'.$dbFile.'1', $dbFile.'1']; - yield ['sqlite3:///'.$dbFile.'3', $dbFile.'3']; - yield ['sqlite://localhost/:memory:']; + yield 'SQLite file' => ['sqlite://localhost/'.$dbFile.'1', $dbFile.'1']; + yield 'SQLite3 file' => ['sqlite3:///'.$dbFile.'3', $dbFile.'3']; + yield 'SQLite in memory' => ['sqlite://localhost/:memory:']; + } + + /** + * @requires extension pdo_pgsql + * + * @group integration + */ + public function testDsnWithPostgreSQL() + { + if (!$host = getenv('POSTGRES_HOST')) { + $this->markTestSkipped('Missing POSTGRES_HOST env variable'); + } + + $key = new Key(uniqid(__METHOD__, true)); + + try { + $store = new DoctrineDbalStore('pgsql://postgres:password@'.$host); + + $store->save($key); + $this->assertTrue($store->exists($key)); + } finally { + $pdo = new \PDO('pgsql:host='.$host.';user=postgres;password=password'); + $pdo->exec('DROP TABLE IF EXISTS lock_keys'); + } } /** diff --git a/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php index 3c006a96660c7..6e7f9e9b7151b 100644 --- a/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php @@ -20,8 +20,6 @@ * @author Jérémy Derussé * * @requires extension pdo_sqlite - * - * @group integration */ class PdoStoreTest extends AbstractStoreTestCase { @@ -72,9 +70,9 @@ public function testInvalidTtlConstruct() } /** - * @dataProvider provideDsn + * @dataProvider provideDsnWithSQLite */ - public function testDsn(string $dsn, string $file = null) + public function testDsnWithSQLite(string $dsn, string $file = null) { $key = new Key(uniqid(__METHOD__, true)); @@ -90,10 +88,36 @@ public function testDsn(string $dsn, string $file = null) } } - public static function provideDsn() + public static function provideDsnWithSQLite() { $dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); - yield ['sqlite:'.$dbFile.'2', $dbFile.'2']; - yield ['sqlite::memory:']; + yield 'SQLite file' => ['sqlite:'.$dbFile.'2', $dbFile.'2']; + yield 'SQLite in memory' => ['sqlite::memory:']; + } + + /** + * @requires extension pdo_pgsql + * + * @group integration + */ + public function testDsnWithPostgreSQL() + { + if (!$host = getenv('POSTGRES_HOST')) { + $this->markTestSkipped('Missing POSTGRES_HOST env variable'); + } + + $key = new Key(uniqid(__METHOD__, true)); + + $dsn = 'pgsql:host='.$host.';user=postgres;password=password'; + + try { + $store = new PdoStore($dsn); + + $store->save($key); + $this->assertTrue($store->exists($key)); + } finally { + $pdo = new \PDO($dsn); + $pdo->exec('DROP TABLE IF EXISTS lock_keys'); + } } } diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php index 0736f0820827d..45f01946d63c0 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php @@ -182,7 +182,8 @@ public function get(): ?array // Wrap the rownum query in a sub-query to allow writelocks without ORA-02014 error if ($this->driverConnection->getDatabasePlatform() instanceof OraclePlatform) { $query = $this->createQueryBuilder('w') - ->where('w.id IN ('.str_replace('SELECT a.* FROM', 'SELECT a.id FROM', $sql).')'); + ->where('w.id IN ('.str_replace('SELECT a.* FROM', 'SELECT a.id FROM', $sql).')') + ->setParameters($query->getParameters()); if (method_exists(QueryBuilder::class, 'forUpdate')) { $query->forUpdate(); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php index 3e8e10928d8d2..8b5a51c745068 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php @@ -64,6 +64,7 @@ public function get(): ?array // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS $this->executeStatement(sprintf('LISTEN "%s"', $this->configuration['table_name'])); + // The condition should be removed once support for DBAL <3.3 is dropped if (method_exists($this->driverConnection, 'getNativeConnection')) { $wrappedConnection = $this->driverConnection->getNativeConnection(); } else { diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php index e665293529433..063f2056793d8 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php @@ -94,7 +94,21 @@ public function __construct(array $options, \Redis|Relay|\RedisCluster $redis = } else { if (null !== $sentinelMaster) { $sentinelClass = \extension_loaded('redis') ? \RedisSentinel::class : Sentinel::class; - $sentinelClient = new $sentinelClass($host, $port, $options['timeout'], $options['persistent_id'], $options['retry_interval'], $options['read_timeout']); + + if (\extension_loaded('redis') && version_compare(phpversion('redis'), '6.0.0', '>=')) { + $params = [ + 'host' => $host, + 'port' => $port, + 'connectTimeout' => $options['timeout'], + 'persistent' => $options['persistent_id'], + 'retryInterval' => $options['retry_interval'], + 'readTimeout' => $options['read_timeout'], + ]; + + $sentinelClient = new \RedisSentinel($params); + } else { + $sentinelClient = new $sentinelClass($host, $port, $options['timeout'], $options['persistent_id'], $options['retry_interval'], $options['read_timeout']); + } if (!$address = $sentinelClient->getMasterAddrByName($sentinelMaster)) { throw new InvalidArgumentException(sprintf('Failed to retrieve master information from master name "%s" and address "%s:%d".', $sentinelMaster, $host, $port)); @@ -235,10 +249,10 @@ private static function parseDsn(string $dsn, array &$options): array $url = preg_replace_callback('#^'.$scheme.':(//)?(?:(?:(?[^:@]*+):)?(?[^@]*+)@)?#', function ($m) use (&$auth) { if (isset($m['password'])) { if (!\in_array($m['user'], ['', 'default'], true)) { - $auth['user'] = $m['user']; + $auth['user'] = rawurldecode($m['user']); } - $auth['pass'] = $m['password']; + $auth['pass'] = rawurldecode($m['password']); } return 'file:'.($m[1] ?? ''); diff --git a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php index f430a28b4bfe2..9746c5ed16848 100644 --- a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php +++ b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php @@ -258,7 +258,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti public function getSubscribedSignals(): array { - return $this->signals ?? (\defined('SIGTERM') ? [\SIGTERM, \SIGINT] : []); + return $this->signals ?? (\extension_loaded('pcntl') ? [\SIGTERM, \SIGINT] : []); } public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php index ae47d774b51c4..53e70bb99534c 100644 --- a/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php +++ b/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php @@ -134,7 +134,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int public function getSubscribedSignals(): array { - return $this->signals ?? (\defined('SIGTERM') ? [\SIGTERM, \SIGINT] : []); + return $this->signals ?? (\extension_loaded('pcntl') ? [\SIGTERM, \SIGINT] : []); } public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index 03f48edfcd93a..032ec76efa5e2 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -106,6 +106,7 @@ private function registerHandlers(ContainerBuilder $container, array $busIds): v unset($options['handles']); $priority = $options['priority'] ?? 0; $method = $options['method'] ?? '__invoke'; + $fromTransport = $options['from_transport'] ?? ''; if (isset($options['bus'])) { if (!\in_array($options['bus'], $busIds)) { @@ -131,10 +132,10 @@ private function registerHandlers(ContainerBuilder $container, array $busIds): v throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::%s()" does not exist.', $serviceId, $r->getName(), $method)); } - if ('__invoke' !== $method) { + if ('__invoke' !== $method || '' !== $fromTransport) { $wrapperDefinition = (new Definition('Closure'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable'); - $definitions[$definitionId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($message.':'.$priority.':'.$serviceId.':'.$method)] = $wrapperDefinition; + $definitions[$definitionId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($message.':'.$priority.':'.$serviceId.':'.$method.':'.$fromTransport)] = $wrapperDefinition; } else { $definitionId = $serviceId; } diff --git a/src/Symfony/Component/Messenger/EventListener/StopWorkerOnSignalsListener.php b/src/Symfony/Component/Messenger/EventListener/StopWorkerOnSignalsListener.php index 368c5c5b61862..327d1bb32b0c2 100644 --- a/src/Symfony/Component/Messenger/EventListener/StopWorkerOnSignalsListener.php +++ b/src/Symfony/Component/Messenger/EventListener/StopWorkerOnSignalsListener.php @@ -26,7 +26,7 @@ class StopWorkerOnSignalsListener implements EventSubscriberInterface public function __construct(array $signals = null, LoggerInterface $logger = null) { - if (null === $signals && \defined('SIGTERM')) { + if (null === $signals && \extension_loaded('pcntl')) { $signals = [SIGTERM, SIGINT]; } $this->signals = $signals ?? []; diff --git a/src/Symfony/Component/Messenger/EventListener/StopWorkerOnSigtermSignalListener.php b/src/Symfony/Component/Messenger/EventListener/StopWorkerOnSigtermSignalListener.php index 0b330338d19be..e1509dcf08358 100644 --- a/src/Symfony/Component/Messenger/EventListener/StopWorkerOnSigtermSignalListener.php +++ b/src/Symfony/Component/Messenger/EventListener/StopWorkerOnSigtermSignalListener.php @@ -24,6 +24,6 @@ class StopWorkerOnSigtermSignalListener extends StopWorkerOnSignalsListener { public function __construct(LoggerInterface $logger = null) { - parent::__construct(\defined('SIGTERM') ? [SIGTERM] : [], $logger); + parent::__construct(\extension_loaded('pcntl') ? [SIGTERM] : [], $logger); } } diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index 226c8d71fb27a..13d18993eb97c 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -52,6 +52,7 @@ use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage; use Symfony\Component\Messenger\Tests\Fixtures\TaggedDummyHandler; use Symfony\Component\Messenger\Tests\Fixtures\TaggedDummyHandlerWithUnionTypes; +use Symfony\Component\Messenger\Tests\Fixtures\ThirdMessage; use Symfony\Component\Messenger\Tests\Fixtures\UnionBuiltinTypeArgumentHandler; use Symfony\Component\Messenger\Tests\Fixtures\UnionTypeArgumentHandler; use Symfony\Component\Messenger\Tests\Fixtures\UnionTypeOneMessage; @@ -102,7 +103,7 @@ public function testFromTransportViaTagAttribute() $container = $this->getContainerBuilder($busId = 'message_bus'); $container ->register(DummyHandler::class, DummyHandler::class) - ->addTag('messenger.message_handler', ['from_transport' => 'async']) + ->addTag('messenger.message_handler', ['from_transport' => 'async', 'method' => '__invoke']) ; (new MessengerPass())->process($container); @@ -113,7 +114,7 @@ public function testFromTransportViaTagAttribute() $handlerDescriptionMapping = $handlersLocatorDefinition->getArgument(0); $this->assertCount(1, $handlerDescriptionMapping); - $this->assertHandlerDescriptor($container, $handlerDescriptionMapping, DummyMessage::class, [DummyHandler::class], [['from_transport' => 'async']]); + $this->assertHandlerDescriptor($container, $handlerDescriptionMapping, DummyMessage::class, [[DummyHandler::class, '__invoke']], [['from_transport' => 'async']]); } public function testHandledMessageTypeResolvedWithMethodAndNoHandlesViaTagAttributes() @@ -178,7 +179,7 @@ public function testTaggedMessageHandler() $this->assertSame(HandlersLocator::class, $handlersLocatorDefinition->getClass()); $handlerDescriptionMapping = $handlersLocatorDefinition->getArgument(0); - $this->assertCount(2, $handlerDescriptionMapping); + $this->assertCount(3, $handlerDescriptionMapping); $this->assertHandlerDescriptor($container, $handlerDescriptionMapping, DummyMessage::class, [TaggedDummyHandler::class], [[]]); $this->assertHandlerDescriptor( @@ -187,6 +188,19 @@ public function testTaggedMessageHandler() SecondMessage::class, [[TaggedDummyHandler::class, 'handleSecondMessage']] ); + $this->assertHandlerDescriptor( + $container, + $handlerDescriptionMapping, + ThirdMessage::class, + [ + [TaggedDummyHandler::class, 'handleThirdMessage'], + [TaggedDummyHandler::class, 'handleThirdMessage'], + ], + [ + ['from_transport' => 'a'], + ['from_transport' => 'b'], + ], + ); } public function testTaggedMessageHandlerWithUnionTypes() diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/TaggedDummyHandler.php b/src/Symfony/Component/Messenger/Tests/Fixtures/TaggedDummyHandler.php index cecd6f2e85d49..794286b2c4daa 100644 --- a/src/Symfony/Component/Messenger/Tests/Fixtures/TaggedDummyHandler.php +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/TaggedDummyHandler.php @@ -15,4 +15,10 @@ public function __invoke(DummyMessage $message) public function handleSecondMessage(SecondMessage $message) { } + + #[AsMessageHandler(fromTransport: 'a')] + #[AsMessageHandler(fromTransport: 'b')] + public function handleThirdMessage(ThirdMessage $message): void + { + } } diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/ThirdMessage.php b/src/Symfony/Component/Messenger/Tests/Fixtures/ThirdMessage.php new file mode 100644 index 0000000000000..b40e7a9c86201 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/ThirdMessage.php @@ -0,0 +1,7 @@ +lexer->tokenize($rawDocNode)); - $phpDocNode = $this->phpDocParser->parse($tokens); - $tokens->consumeTokenType(Lexer::TOKEN_END); + $phpDocNode = $this->getPhpDocNode($rawDocNode); return $this->filterDocBlockParams($phpDocNode, $property); } @@ -239,24 +237,27 @@ private function getDocBlockFromProperty(string $class, string $property): ?arra return null; } + // Type can be inside property docblock as `@var` + $rawDocNode = $reflectionProperty->getDocComment(); + $phpDocNode = $rawDocNode ? $this->getPhpDocNode($rawDocNode) : null; $source = self::PROPERTY; - if ($reflectionProperty->isPromoted()) { + if (!$phpDocNode?->getTagsByName('@var')) { + $phpDocNode = null; + } + + // or in the constructor as `@param` for promoted properties + if (!$phpDocNode && $reflectionProperty->isPromoted()) { $constructor = new \ReflectionMethod($class, '__construct'); $rawDocNode = $constructor->getDocComment(); + $phpDocNode = $rawDocNode ? $this->getPhpDocNode($rawDocNode) : null; $source = self::MUTATOR; - } else { - $rawDocNode = $reflectionProperty->getDocComment(); } - if (!$rawDocNode) { + if (!$phpDocNode) { return null; } - $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode)); - $phpDocNode = $this->phpDocParser->parse($tokens); - $tokens->consumeTokenType(Lexer::TOKEN_END); - return [$phpDocNode, $source, $reflectionProperty->class]; } @@ -296,10 +297,17 @@ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, i return null; } + $phpDocNode = $this->getPhpDocNode($rawDocNode); + + return [$phpDocNode, $prefix, $reflectionMethod->class]; + } + + private function getPhpDocNode(string $rawDocNode): PhpDocNode + { $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode)); $phpDocNode = $this->phpDocParser->parse($tokens); $tokens->consumeTokenType(Lexer::TOKEN_END); - return [$phpDocNode, $prefix, $reflectionMethod->class]; + return $phpDocNode; } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index f3405d0409ae3..4aade0e11f7d4 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -474,6 +474,8 @@ public function testExtractPhp80Type(string $class, $property, array $type = nul public static function php80TypesProvider() { return [ + [Php80Dummy::class, 'promotedWithDocCommentAndType', [new Type(Type::BUILTIN_TYPE_INT)]], + [Php80Dummy::class, 'promotedWithDocComment', [new Type(Type::BUILTIN_TYPE_STRING)]], [Php80Dummy::class, 'promotedAndMutated', [new Type(Type::BUILTIN_TYPE_STRING)]], [Php80Dummy::class, 'promoted', null], [Php80Dummy::class, 'collection', [new Type(Type::BUILTIN_TYPE_ARRAY, collection: true, collectionValueType: new Type(Type::BUILTIN_TYPE_STRING))]], diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php index dc985fea0b212..1bf93ba70dbb0 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php @@ -17,9 +17,23 @@ class Php80Dummy /** * @param string $promotedAndMutated + * @param string $promotedWithDocComment + * @param string $promotedWithDocCommentAndType * @param array $collection */ - public function __construct(private mixed $promoted, private mixed $promotedAndMutated, private array $collection) + public function __construct( + private mixed $promoted, + private mixed $promotedAndMutated, + /** + * Comment without @var. + */ + private mixed $promotedWithDocComment, + /** + * @var int + */ + private mixed $promotedWithDocCommentAndType, + private array $collection, + ) { } diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index 80a3a932131d3..26ee3237c01d3 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -135,26 +135,22 @@ public function decode(string $data, string $format, array $context = []): mixed // todo: throw an exception if the root node name is not correctly configured (bc) if ($rootNode->hasChildNodes()) { - $xpath = new \DOMXPath($dom); - $data = []; - foreach ($xpath->query('namespace::*', $dom->documentElement) as $nsNode) { - $data['@'.$nsNode->nodeName] = $nsNode->nodeValue; + $data = $this->parseXml($rootNode, $context); + if (\is_array($data)) { + $data = $this->addXmlNamespaces($data, $rootNode, $dom); } - unset($data['@xmlns:xml']); - - if (empty($data)) { - return $this->parseXml($rootNode, $context); - } - - return array_merge($data, (array) $this->parseXml($rootNode, $context)); + return $data; } if (!$rootNode->hasAttributes()) { return $rootNode->nodeValue; } - return array_merge($this->parseXmlAttributes($rootNode, $context), ['#' => $rootNode->nodeValue]); + $data = array_merge($this->parseXmlAttributes($rootNode, $context), ['#' => $rootNode->nodeValue]); + $data = $this->addXmlNamespaces($data, $rootNode, $dom); + + return $data; } public function supportsEncoding(string $format): bool @@ -326,6 +322,19 @@ private function parseXmlValue(\DOMNode $node, array $context = []): array|strin return $value; } + private function addXmlNamespaces(array $data, \DOMNode $node, \DOMDocument $document): array + { + $xpath = new \DOMXPath($document); + + foreach ($xpath->query('namespace::*', $node) as $nsNode) { + $data['@'.$nsNode->nodeName] = $nsNode->nodeValue; + } + + unset($data['@xmlns:xml']); + + return $data; + } + /** * Parse the data and convert it to DOMElements. * diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 9a0fb4ed28993..2e11136ba0b87 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -341,6 +341,8 @@ protected function instantiateObject(array &$data, string $class, array &$contex $constructorParameters = $constructor->getParameters(); $missingConstructorArguments = []; $params = []; + $unsetKeys = []; + foreach ($constructorParameters as $constructorParameter) { $paramName = $constructorParameter->name; $attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context); @@ -360,18 +362,17 @@ protected function instantiateObject(array &$data, string $class, array &$contex } $params = array_merge($params, $variadicParameters); - unset($data[$key]); + $unsetKeys[] = $key; } } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { $parameterData = $data[$key]; if (null === $parameterData && $constructorParameter->allowsNull()) { $params[] = null; - // Don't run set for a parameter passed to the constructor - unset($data[$key]); + $unsetKeys[] = $key; + continue; } - // Don't run set for a parameter passed to the constructor try { $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format); } catch (NotNormalizableValueException $exception) { @@ -382,7 +383,8 @@ protected function instantiateObject(array &$data, string $class, array &$contex $context['not_normalizable_value_exceptions'][] = $exception; $params[] = $parameterData; } - unset($data[$key]); + + $unsetKeys[] = $key; } elseif (\array_key_exists($key, $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) { $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; } elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) { @@ -413,11 +415,25 @@ protected function instantiateObject(array &$data, string $class, array &$contex } if (!$constructor->isConstructor()) { - return $constructor->invokeArgs(null, $params); + $instance = $constructor->invokeArgs(null, $params); + + // do not set a parameter that has been set in the constructor + foreach ($unsetKeys as $key) { + unset($data[$key]); + } + + return $instance; } try { - return $reflectionClass->newInstanceArgs($params); + $instance = $reflectionClass->newInstanceArgs($params); + + // do not set a parameter that has been set in the constructor + foreach ($unsetKeys as $key) { + unset($data[$key]); + } + + return $instance; } catch (\TypeError $e) { if (!isset($context['not_normalizable_value_exceptions'])) { throw $e; diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index e6efb49833d0f..34068030bb2d3 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -187,7 +187,9 @@ public function normalize(mixed $object, string $format = null, array $context = $attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context); try { - $attributeValue = $this->getAttributeValue($object, $attribute, $format, $attributeContext); + $attributeValue = $attribute === $this->classDiscriminatorResolver?->getMappingForMappedObject($object)?->getTypeProperty() + ? $this->classDiscriminatorResolver?->getTypeForMappedObject($object) + : $this->getAttributeValue($object, $attribute, $format, $attributeContext); } catch (UninitializedPropertyException $e) { if ($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) { continue; @@ -364,8 +366,12 @@ public function denormalize(mixed $data, string $type, string $format = null, ar } if ($attributeContext[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) { + $discriminatorMapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object); + try { - $attributeContext[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object, $attribute, $format, $attributeContext); + $attributeContext[self::OBJECT_TO_POPULATE] = $attribute === $discriminatorMapping?->getTypeProperty() + ? $discriminatorMapping + : $this->getAttributeValue($object, $attribute, $format, $attributeContext); } catch (NoSuchPropertyException) { } } @@ -432,8 +438,10 @@ private function validateAndDenormalize(array $types, string $currentClass, stri { $expectedTypes = []; $isUnionType = \count($types) > 1; + $e = null; $extraAttributesException = null; $missingConstructorArgumentsException = null; + $isNullable = false; foreach ($types as $type) { if (null === $data && $type->isNullable()) { return null; @@ -456,18 +464,22 @@ private function validateAndDenormalize(array $types, string $currentClass, stri // In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine, // if a value is meant to be a string, float, int or a boolean value from the serialized representation. // That's why we have to transform the values, if one of these non-string basic datatypes is expected. + $builtinType = $type->getBuiltinType(); if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) { if ('' === $data) { - if (Type::BUILTIN_TYPE_ARRAY === $builtinType = $type->getBuiltinType()) { + if (Type::BUILTIN_TYPE_ARRAY === $builtinType) { return []; } - if ($type->isNullable() && \in_array($builtinType, [Type::BUILTIN_TYPE_BOOL, Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true)) { - return null; + if (Type::BUILTIN_TYPE_STRING === $builtinType) { + return ''; } + + // Don't return null yet because Object-types that come first may accept empty-string too + $isNullable = $isNullable ?: $type->isNullable(); } - switch ($builtinType ?? $type->getBuiltinType()) { + switch ($builtinType) { case Type::BUILTIN_TYPE_BOOL: // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1" if ('false' === $data || '0' === $data) { @@ -564,17 +576,17 @@ private function validateAndDenormalize(array $types, string $currentClass, stri return $data; } } catch (NotNormalizableValueException|InvalidArgumentException $e) { - if (!$isUnionType) { + if (!$isUnionType && !$isNullable) { throw $e; } } catch (ExtraAttributesException $e) { - if (!$isUnionType) { + if (!$isUnionType && !$isNullable) { throw $e; } $extraAttributesException ??= $e; } catch (MissingConstructorArgumentsException $e) { - if (!$isUnionType) { + if (!$isUnionType && !$isNullable) { throw $e; } @@ -582,6 +594,10 @@ private function validateAndDenormalize(array $types, string $currentClass, stri } } + if ($isNullable) { + return null; + } + if ($extraAttributesException) { throw $extraAttributesException; } @@ -590,6 +606,10 @@ private function validateAndDenormalize(array $types, string $currentClass, stri throw $missingConstructorArgumentsException; } + if (!$isUnionType && $e) { + throw $e; + } + if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { return $data; } @@ -629,7 +649,7 @@ private function getTypes(string $currentClass, string $attribute): ?array return $this->typesCache[$key] = $types; } - if (null !== $this->classDiscriminatorResolver && null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($currentClass)) { + if ($discriminatorMapping = $this->classDiscriminatorResolver?->getMappingForClass($currentClass)) { if ($discriminatorMapping->getTypeProperty() === $attribute) { return $this->typesCache[$key] = [ new Type(Type::BUILTIN_TYPE_STRING), diff --git a/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php index e05f6fcf3e860..202f0e8614742 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php @@ -131,6 +131,6 @@ public function supportsDenormalization(mixed $data, string $type, string $forma private function isISO8601(string $string): bool { - return preg_match('/^[\-+]?P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:(?:\d+D|%[dD]D)|(?:\d+W|%[wW]W))?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string); + return preg_match('/^[\-+]?P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:\d+W|%[wW]W)?(?:\d+D|%[dD]D)?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index 9a412ff23e0ed..b47ea6a5fb668 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -76,6 +76,10 @@ public function hasCacheableSupportsMethod(): bool */ private function supports(string $class): bool { + if ($this->classDiscriminatorResolver?->getMappingForClass($class)) { + return true; + } + $class = new \ReflectionClass($class); $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC); foreach ($methods as $method) { diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index dd601b828b6b8..3f4638ca66a4e 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -32,9 +32,6 @@ class ObjectNormalizer extends AbstractObjectNormalizer { protected $propertyAccessor; - /** @var array */ - private array $discriminatorCache = []; - private readonly \Closure $objectClassResolver; public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = []) @@ -130,16 +127,11 @@ protected function extractAttributes(object $object, string $format = null, arra protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed { - $cacheKey = $object::class; - if (!\array_key_exists($cacheKey, $this->discriminatorCache)) { - $this->discriminatorCache[$cacheKey] = null; - if (null !== $this->classDiscriminatorResolver) { - $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object); - $this->discriminatorCache[$cacheKey] = $mapping?->getTypeProperty(); - } - } + $mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object); - return $attribute === $this->discriminatorCache[$cacheKey] ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) : $this->propertyAccessor->getValue($object, $attribute); + return $attribute === $mapping?->getTypeProperty() + ? $mapping + : $this->propertyAccessor->getValue($object, $attribute); } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index 7e7743f5e2865..7b3a26426d4a7 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -92,6 +92,10 @@ public function hasCacheableSupportsMethod(): bool */ private function supports(string $class): bool { + if ($this->classDiscriminatorResolver?->getMappingForClass($class)) { + return true; + } + $class = new \ReflectionClass($class); // We look for at least one non-static property diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php index 0f6bb1dc27ceb..ff28cbaafa666 100644 --- a/src/Symfony/Component/Serializer/Serializer.php +++ b/src/Symfony/Component/Serializer/Serializer.php @@ -224,8 +224,20 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $context['not_normalizable_value_exceptions'] = []; $errors = &$context['not_normalizable_value_exceptions']; $denormalized = $normalizer->denormalize($data, $type, $format, $context); + if ($errors) { - throw new PartialDenormalizationException($denormalized, $errors); + // merge errors so that one path has only one error + $uniqueErrors = []; + foreach ($errors as $error) { + if (null === $error->getPath()) { + $uniqueErrors[] = $error; + continue; + } + + $uniqueErrors[$error->getPath()] = $uniqueErrors[$error->getPath()] ?? $error; + } + + throw new PartialDenormalizationException($denormalized, array_values($uniqueErrors)); } return $denormalized; diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php index 9e6601b53c0ce..e83cf4f0648d0 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php @@ -452,6 +452,17 @@ public function testDecodeWithNamespace() $array = $this->getNamespacedArray(); $this->assertEquals($array, $this->encoder->decode($source, 'xml')); + + $source = ''."\n". + ''. + ''."\n"; + + $this->assertEquals([ + '@xmlns' => 'http://www.w3.org/2005/Atom', + '@xmlns:app' => 'http://www.w3.org/2007/app', + '@app:foo' => 'bar', + '#' => '', + ], $this->encoder->decode($source, 'xml')); } public function testDecodeScalarWithAttribute() diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php new file mode 100644 index 0000000000000..15bcc6e6bec7f --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +use Symfony\Component\Serializer\Normalizer\DenormalizableInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + +/** + * @author Jeroen + */ +class DummyString implements DenormalizableInterface +{ + /** @var string $value */ + public $value; + + public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = []): void + { + $this->value = $data; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithNotNormalizable.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithNotNormalizable.php new file mode 100644 index 0000000000000..c961b1c384120 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithNotNormalizable.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\Serializer\Tests\Fixtures; + +/** + * @author Jeroen + */ +class DummyWithNotNormalizable +{ + public function __construct(public NotNormalizableDummy|null $value) + { + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithObjectOrBool.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithObjectOrBool.php new file mode 100644 index 0000000000000..502f32968cc15 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithObjectOrBool.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\Serializer\Tests\Fixtures; + +/** + * @author Jeroen + */ +class DummyWithObjectOrBool +{ + public function __construct(public Php80WithPromotedTypedConstructor|bool $value) + { + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithObjectOrNull.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithObjectOrNull.php new file mode 100644 index 0000000000000..1f74f2fbad3fa --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithObjectOrNull.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\Serializer\Tests\Fixtures; + +/** + * @author Jeroen + */ +class DummyWithObjectOrNull +{ + public function __construct(public Php80WithPromotedTypedConstructor|null $value) + { + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithStringObject.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithStringObject.php new file mode 100644 index 0000000000000..82efbb19003e9 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyWithStringObject.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\Serializer\Tests\Fixtures; + +/** + * @author Jeroen + */ +class DummyWithStringObject +{ + public function __construct(public DummyString|null $value) + { + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php new file mode 100644 index 0000000000000..e8c64f57752dd --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Normalizer\DenormalizableInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + +/** + * @author Jeroen + */ +class NotNormalizableDummy implements DenormalizableInterface +{ + public function __construct() + { + } + + public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = []): void + { + throw new NotNormalizableValueException(); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php index ad32fd70565ee..ed3c495772e03 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php @@ -45,7 +45,7 @@ public function __construct($constructorArgument) final class Php74FullWithTypedConstructor { - public function __construct(float $something) + public function __construct(float $something, bool $somethingElse) { } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 97f96635167a7..16ed0c2dd812c 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -24,6 +24,7 @@ use Symfony\Component\Serializer\Exception\ExtraAttributesException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; @@ -38,6 +39,7 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; +use Symfony\Component\Serializer\Normalizer\CustomNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -49,6 +51,11 @@ use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummySecondChild; use Symfony\Component\Serializer\Tests\Fixtures\DummyFirstChildQuux; use Symfony\Component\Serializer\Tests\Fixtures\DummySecondChildQuux; +use Symfony\Component\Serializer\Tests\Fixtures\DummyString; +use Symfony\Component\Serializer\Tests\Fixtures\DummyWithNotNormalizable; +use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrBool; +use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull; +use Symfony\Component\Serializer\Tests\Fixtures\DummyWithStringObject; use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectDummyWithContextAttribute; class AbstractObjectNormalizerTest extends TestCase @@ -835,6 +842,75 @@ public function testDenormalizeWithCorrectOrderOfAttributeAndProperty() $test = $normalizer->denormalize($data, $obj::class); $this->assertSame('nested-id', $test->id); } + + public function testNormalizeBasedOnAllowedAttributes() + { + $normalizer = new class() extends AbstractObjectNormalizer { + protected function getAllowedAttributes($classOrObject, array $context, bool $attributesAsString = false): array + { + return ['foo']; + } + + protected function extractAttributes(object $object, string $format = null, array $context = []): array + { + return []; + } + + protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed + { + return $object->$attribute; + } + + protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []): void + { + } + }; + + $object = new Dummy(); + $object->foo = 'foo'; + $object->bar = 'bar'; + + $this->assertSame(['foo' => 'foo'], $normalizer->normalize($object)); + } + + public function testDenormalizeUntypedFormat() + { + $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $actual = $serializer->denormalize(['value' => ''], DummyWithObjectOrNull::class, 'xml'); + + $this->assertEquals(new DummyWithObjectOrNull(null), $actual); + } + + public function testDenormalizeUntypedFormatNotNormalizable() + { + $this->expectException(NotNormalizableValueException::class); + $serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $serializer->denormalize(['value' => 'test'], DummyWithNotNormalizable::class, 'xml'); + } + + public function testDenormalizeUntypedFormatMissingArg() + { + $this->expectException(MissingConstructorArgumentsException::class); + $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $serializer->denormalize(['value' => 'invalid'], DummyWithObjectOrNull::class, 'xml'); + } + + public function testDenormalizeUntypedFormatScalar() + { + $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $actual = $serializer->denormalize(['value' => 'false'], DummyWithObjectOrBool::class, 'xml'); + + $this->assertEquals(new DummyWithObjectOrBool(false), $actual); + } + + public function testDenormalizeUntypedStringObject() + { + $serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $actual = $serializer->denormalize(['value' => ''], DummyWithStringObject::class, 'xml'); + + $this->assertEquals(new DummyWithStringObject(new DummyString()), $actual); + $this->assertEquals('', $actual->value->value); + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateIntervalNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateIntervalNormalizerTest.php index fe59e098bdbf5..a4ecfd4c22c86 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateIntervalNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateIntervalNormalizerTest.php @@ -119,6 +119,16 @@ public function testDenormalizeIntervalsWithOmittedPartsBeingZero() $this->assertDateIntervalEquals($this->getInterval('P0Y0M0DT12H34M0S'), $normalizer->denormalize('PT12H34M', \DateInterval::class)); } + public function testDenormalizeIntervalWithBothWeeksAndDays() + { + $input = 'P1W1D'; + $interval = $this->normalizer->denormalize($input, \DateInterval::class, null, [ + DateIntervalNormalizer::FORMAT_KEY => '%rP%yY%mM%wW%dDT%hH%iM%sS', + ]); + $this->assertDateIntervalEquals($this->getInterval($input), $interval); + $this->assertSame(8, $interval->d); + } + public function testDenormalizeExpectsString() { $this->expectException(NotNormalizableValueException::class); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php index 476f2a353338f..72652f340115a 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php @@ -58,7 +58,7 @@ public function testMetadataAwareNameConvertorWithNotSerializedConstructorParame public function testConstructorWithMissingData() { $data = [ - 'foo' => 10, + 'bar' => 10, ]; $normalizer = $this->getDenormalizerForConstructArguments(); @@ -66,16 +66,16 @@ public function testConstructorWithMissingData() $normalizer->denormalize($data, ConstructorArgumentsObject::class); self::fail(sprintf('Failed asserting that exception of type "%s" is thrown.', MissingConstructorArgumentsException::class)); } catch (MissingConstructorArgumentsException $e) { - self::assertSame(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$bar", "$baz".', ConstructorArgumentsObject::class), $e->getMessage()); self::assertSame(ConstructorArgumentsObject::class, $e->getClass()); - self::assertSame(['bar', 'baz'], $e->getMissingConstructorArguments()); + self::assertSame(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$foo", "$baz".', ConstructorArgumentsObject::class), $e->getMessage()); + self::assertSame(['foo', 'baz'], $e->getMissingConstructorArguments()); } } public function testExceptionsAreCollectedForConstructorWithMissingData() { $data = [ - 'foo' => 10, + 'bar' => 10, ]; $exceptions = []; @@ -86,7 +86,7 @@ public function testExceptionsAreCollectedForConstructorWithMissingData() ]); self::assertCount(2, $exceptions); - self::assertSame('Failed to create object because the class misses the "bar" property.', $exceptions[0]->getMessage()); + self::assertSame('Failed to create object because the class misses the "foo" property.', $exceptions[0]->getMessage()); self::assertSame('Failed to create object because the class misses the "baz" property.', $exceptions[1]->getMessage()); } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index e7ce846163e4d..ccbb7be0e56f6 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -16,7 +16,9 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; @@ -495,6 +497,27 @@ protected function getNormalizerForSkipUninitializedValues(): NormalizerInterfac { return new GetSetMethodNormalizer(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); } + + public function testNormalizeWithDiscriminator() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, null, $discriminator); + + $this->assertSame(['type' => 'one', 'url' => 'URL_ONE'], $normalizer->normalize(new GetSetMethodDiscriminatedDummyOne())); + } + + public function testDenormalizeWithDiscriminator() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, null, $discriminator); + + $denormalized = new GetSetMethodDiscriminatedDummyTwo(); + $denormalized->setUrl('url'); + + $this->assertEquals($denormalized, $normalizer->denormalize(['type' => 'two', 'url' => 'url'], GetSetMethodDummyInterface::class)); + } } class GetSetDummy @@ -759,3 +782,43 @@ public function __call($key, $value) throw new \RuntimeException('__call should not be called. Called with: '.$key); } } + +/** + * @DiscriminatorMap(typeProperty="type", mapping={ + * "one" = GetSetMethodDiscriminatedDummyOne::class, + * "two" = GetSetMethodDiscriminatedDummyTwo::class, + * }) + */ +interface GetSetMethodDummyInterface +{ +} + +class GetSetMethodDiscriminatedDummyOne implements GetSetMethodDummyInterface +{ + private $url = 'URL_ONE'; + + public function getUrl(): string + { + return $this->url; + } + + public function setUrl(string $url): void + { + $this->url = $url; + } +} + +class GetSetMethodDiscriminatedDummyTwo implements GetSetMethodDummyInterface +{ + private $url = 'URL_TWO'; + + public function getUrl(): string + { + return $this->url; + } + + public function setUrl(string $url): void + { + $this->url = $url; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index f1401158fb140..915622f2f233c 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -16,7 +16,9 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; @@ -502,6 +504,27 @@ protected function getNormalizerForSkipUninitializedValues(): NormalizerInterfac { return new PropertyNormalizer(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); } + + public function testNormalizeWithDiscriminator() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new PropertyNormalizer($classMetadataFactory, null, null, $discriminator); + + $this->assertSame(['type' => 'one', 'url' => 'URL_ONE'], $normalizer->normalize(new PropertyDiscriminatedDummyOne())); + } + + public function testDenormalizeWithDiscriminator() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new PropertyNormalizer($classMetadataFactory, null, null, $discriminator); + + $denormalized = new PropertyDiscriminatedDummyTwo(); + $denormalized->url = 'url'; + + $this->assertEquals($denormalized, $normalizer->denormalize(['type' => 'two', 'url' => 'url'], PropertyDummyInterface::class)); + } } class PropertyDummy @@ -605,3 +628,23 @@ public function getIntMatrix(): array return $this->intMatrix; } } + +/** + * @DiscriminatorMap(typeProperty="type", mapping={ + * "one" = PropertyDiscriminatedDummyOne::class, + * "two" = PropertyDiscriminatedDummyTwo::class, + * }) + */ +interface PropertyDummyInterface +{ +} + +class PropertyDiscriminatedDummyOne implements PropertyDummyInterface +{ + public $url = 'URL_ONE'; +} + +class PropertyDiscriminatedDummyTwo implements PropertyDummyInterface +{ + public $url = 'URL_TWO'; +} diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index a491c682c698a..bce705af22307 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -17,6 +17,7 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Exception\ExtraAttributesException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; @@ -58,6 +59,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo; use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor; use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty; +use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull; use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy; use Symfony\Component\Serializer\Tests\Fixtures\FooImplementationDummy; use Symfony\Component\Serializer\Tests\Fixtures\FooInterfaceDummyDenormalizer; @@ -843,6 +845,14 @@ public function testTrueBuiltInTypes() $this->assertEquals(new TrueBuiltInDummy(), $actual); } + public function testDeserializeUntypedFormat() + { + $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))], ['csv' => new CsvEncoder()]); + $actual = $serializer->deserialize('value'.\PHP_EOL.',', DummyWithObjectOrNull::class, 'csv', [CsvEncoder::AS_COLLECTION_KEY => false]); + + $this->assertEquals(new DummyWithObjectOrNull(null), $actual); + } + private function serializerWithClassDiscriminator() { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); @@ -891,7 +901,8 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet ], "php74FullWithConstructor": {}, "php74FullWithTypedConstructor": { - "something": "not a float" + "something": "not a float", + "somethingElse": "not a bool" }, "dummyMessage": { }, @@ -1053,6 +1064,15 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet 'useMessageForUser' => false, 'message' => 'The type of the "something" attribute for class "Symfony\Component\Serializer\Tests\Fixtures\Php74FullWithTypedConstructor" must be one of "float" ("string" given).', ], + [ + 'currentType' => 'string', + 'expectedTypes' => [ + 'bool', + ], + 'path' => 'php74FullWithTypedConstructor.somethingElse', + 'useMessageForUser' => false, + 'message' => 'The type of the "somethingElse" attribute for class "Symfony\Component\Serializer\Tests\Fixtures\Php74FullWithTypedConstructor" must be one of "bool" ("string" given).', + ], $classMetadataFactory ? [ 'currentType' => 'null', diff --git a/src/Symfony/Component/String/Inflector/EnglishInflector.php b/src/Symfony/Component/String/Inflector/EnglishInflector.php index 2cd6bb87b6eaa..feea7f668df04 100644 --- a/src/Symfony/Component/String/Inflector/EnglishInflector.php +++ b/src/Symfony/Component/String/Inflector/EnglishInflector.php @@ -21,7 +21,7 @@ final class EnglishInflector implements InflectorInterface private const PLURAL_MAP = [ // First entry: plural suffix, reversed // Second entry: length of plural suffix - // Third entry: Whether the suffix may succeed a vocal + // Third entry: Whether the suffix may succeed a vowel // Fourth entry: Whether the suffix may succeed a consonant // Fifth entry: singular suffix, normal @@ -162,7 +162,7 @@ final class EnglishInflector implements InflectorInterface private const SINGULAR_MAP = [ // First entry: singular suffix, reversed // Second entry: length of singular suffix - // Third entry: Whether the suffix may succeed a vocal + // Third entry: Whether the suffix may succeed a vowel // Fourth entry: Whether the suffix may succeed a consonant // Fifth entry: plural suffix, normal @@ -253,6 +253,9 @@ final class EnglishInflector implements InflectorInterface // seasons (season), treasons (treason), poisons (poison), lessons (lesson) ['nos', 3, true, true, 'sons'], + // icons (icon) + ['noc', 3, true, true, 'cons'], + // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) ['no', 2, true, true, 'a'], @@ -343,15 +346,30 @@ final class EnglishInflector implements InflectorInterface // deer 'reed', + // equipment + 'tnempiuqe', + // feedback 'kcabdeef', // fish 'hsif', + // health + 'htlaeh', + + // history + 'yrotsih', + // info 'ofni', + // information + 'noitamrofni', + + // money + 'yenom', + // moose 'esoom', @@ -363,6 +381,9 @@ final class EnglishInflector implements InflectorInterface // species 'seiceps', + + // traffic + 'ciffart', ]; public function singularize(string $plural): array @@ -396,14 +417,14 @@ public function singularize(string $plural): array if ($j === $suffixLength) { // Is there any character preceding the suffix in the plural string? if ($j < $pluralLength) { - $nextIsVocal = str_contains('aeiou', $lowerPluralRev[$j]); + $nextIsVowel = str_contains('aeiou', $lowerPluralRev[$j]); - if (!$map[2] && $nextIsVocal) { - // suffix may not succeed a vocal but next char is one + if (!$map[2] && $nextIsVowel) { + // suffix may not succeed a vowel but next char is one break; } - if (!$map[3] && !$nextIsVocal) { + if (!$map[3] && !$nextIsVowel) { // suffix may not succeed a consonant but next char is one break; } @@ -473,14 +494,14 @@ public function pluralize(string $singular): array if ($j === $suffixLength) { // Is there any character preceding the suffix in the plural string? if ($j < $singularLength) { - $nextIsVocal = str_contains('aeiou', $lowerSingularRev[$j]); + $nextIsVowel = str_contains('aeiou', $lowerSingularRev[$j]); - if (!$map[2] && $nextIsVocal) { - // suffix may not succeed a vocal but next char is one + if (!$map[2] && $nextIsVowel) { + // suffix may not succeed a vowel but next char is one break; } - if (!$map[3] && !$nextIsVocal) { + if (!$map[3] && !$nextIsVowel) { // suffix may not succeed a consonant but next char is one break; } diff --git a/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php b/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php index 6c7a1c7c6e895..cf66bf05b660c 100644 --- a/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php +++ b/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php @@ -295,6 +295,7 @@ public static function pluralizeProvider() ['tree', 'trees'], ['waltz', 'waltzes'], ['wife', 'wives'], + ['icon', 'icons'], // test casing: if the first letter was uppercase, it should remain so ['Man', 'Men'], diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php index e4f9b20cf1722..e5977c7507332 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php @@ -145,7 +145,6 @@ private function exportFiles(array $locales, array $domains): array 'json' => [ 'format' => 'symfony_xliff', 'original_filenames' => true, - 'directory_prefix' => '%LANG_ISO%', 'filter_langs' => array_values($locales), 'filter_filenames' => array_map($this->getLokaliseFilenameFromDomain(...), $domains), 'export_empty_as' => 'skip', @@ -165,7 +164,12 @@ private function exportFiles(array $locales, array $domains): array throw new ProviderException(sprintf('Unable to export translations from Lokalise: "%s".', $response->getContent(false)), $response); } - return $responseContent['files']; + // Lokalise returns languages with "-" separator, we need to reformat them to "_" separator. + $reformattedLanguages = array_map(function ($language) { + return str_replace('-', '_', $language); + }, array_keys($responseContent['files'])); + + return array_combine($reformattedLanguages, $responseContent['files']); } private function createKeys(array $keys, string $domain): array diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php b/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php index 979e73ffb0d3e..99bf8ac32f293 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php @@ -560,7 +560,6 @@ public function testReadForOneLocaleAndOneDomain(string $locale, string $domain, $expectedBody = json_encode([ 'format' => 'symfony_xliff', 'original_filenames' => true, - 'directory_prefix' => '%LANG_ISO%', 'filter_langs' => [$locale], 'filter_filenames' => [$domain.'.xliff'], 'export_empty_as' => 'skip', @@ -582,15 +581,10 @@ public function testReadForOneLocaleAndOneDomain(string $locale, string $domain, ])); }; - $loader = $this->getLoader(); - $loader->expects($this->once()) - ->method('load') - ->willReturn((new XliffFileLoader())->load($responseContent, $locale, $domain)); - $provider = self::createProvider((new MockHttpClient($response))->withOptions([ 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/', 'headers' => ['X-Api-Token' => 'API_KEY'], - ]), $loader, $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com'); + ]), new XliffFileLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com'); $translatorBag = $provider->read([$domain], [$locale]); // We don't want to assert equality of metadata here, due to the ArrayLoader usage. @@ -762,6 +756,36 @@ public static function getResponsesForOneLocaleAndOneDomain(): \Generator $expectedTranslatorBagEn, ]; + $expectedTranslatorBagEnUS = new TranslatorBag(); + $expectedTranslatorBagEnUS->addCatalogue($arrayLoader->load([ + 'index.hello' => 'Hello', + 'index.greetings' => 'Welcome, {firstname}!', + ], 'en_US')); + + yield ['en_US', 'messages', <<<'XLIFF' + + + +
+ +
+ + + index.greetings + Welcome, {firstname}! + + + index.hello + Hello + + +
+
+XLIFF + , + $expectedTranslatorBagEnUS, + ]; + $expectedTranslatorBagFr = new TranslatorBag(); $expectedTranslatorBagFr->addCatalogue($arrayLoader->load([ 'index.hello' => 'Bonjour', diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.bg.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.bg.xlf index 455ff81679a1b..d9efdf5751fe1 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.bg.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.bg.xlf @@ -402,6 +402,30 @@ The value of the netmask should be between {{ min }} and {{ max }}. Стойността на мрежовата маска трябва да бъде между {{ min }} и {{ max }}. + + The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less. + Името на файла е твърде дълго. Трябва да съдържа не повече от {{ filename_max_length }} символ.|Името на файла е твърде дълго. Трябва да съдържа не повече от {{ filename_max_length }} символа. + + + The password strength is too low. Please use a stronger password. + Сложността на паролата е твърде малка. Моля използвайте по-сложна парола. + + + This value contains characters that are not allowed by the current restriction-level. + Стойността съдържа символи, които не са позволени от текущото ниво на ограничение. + + + Using invisible characters is not allowed. + Използването на невидими символи не е позволено. + + + Mixing numbers from different scripts is not allowed. + Смесването на числа от различни скриптове не е позволено. + + + Using hidden overlay characters is not allowed. + Използването на скрити насложени символи не е позволено. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf index 75410192190ef..d53747e2aef70 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf @@ -402,6 +402,30 @@ The value of the netmask should be between {{ min }} and {{ max }}. Hodnota masky sítě musí být mezi {{ min }} a {{ max }}. + + The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less. + Název souboru je příliš dlouhý. Měl by obsahovat {{ filename_max_length }} znak nebo méně.|Název souboru je příliš dlouhý. Měl by obsahovat {{ filename_max_length }} znaky nebo méně.|Název souboru je příliš dlouhý. Měl by obsahovat {{ filename_max_length }} znaků nebo méně. + + + The password strength is too low. Please use a stronger password. + Síla hesla je příliš nízká. Použijte silnější heslo, prosím. + + + This value contains characters that are not allowed by the current restriction-level. + Tato hodnota obsahuje znaky, které nejsou povoleny aktuální úrovní omezení. + + + Using invisible characters is not allowed. + Používání neviditelných znaků není povoleno. + + + Mixing numbers from different scripts is not allowed. + Kombinování čísel z různých písem není povoleno. + + + Using hidden overlay characters is not allowed. + Použití skrytých překrývajících znaků není povoleno. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf index 7a2c4c521b56a..32b379e300495 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf @@ -402,6 +402,30 @@ The value of the netmask should be between {{ min }} and {{ max }}. Tinklo kaukės reikšmė turi būti nuo {{ min }} iki {{ max }}. + + The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less. + Failo pavadinimas per ilgas. Jame turėtų būti {{ filename_max_length }} simbolis arba mažiau.|Failo pavadinimas per ilgas. Jame turėtų būti {{ filename_max_length }} simbolių arba mažiau. + + + The password strength is too low. Please use a stronger password. + Slaptažodis per silpnas. Naudokite stipresnį slaptažodį. + + + This value contains characters that are not allowed by the current restriction-level. + Šioje reikšmėje yra simbolių, kurių neleidžia dabartinis apribojimo lygis. + + + Using invisible characters is not allowed. + Naudoti nematomus simbolius draudžiama. + + + Mixing numbers from different scripts is not allowed. + Draudžiama maišyti skaičius iš skirtingų scenarijų. + + + Using hidden overlay characters is not allowed. + Draudžiama naudoti paslėptus perdangos simbolius. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf index 715137d5890a9..09e841565504f 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf @@ -402,6 +402,30 @@ The value of the netmask should be between {{ min }} and {{ max }}. Netmask'in değeri {{ min }} ve {{ max }} arasında olmaldır. + + The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less. + Dosya adı çok uzun. {{ filename_max_length }} karakter veya daha az olmalıdır. + + + The password strength is too low. Please use a stronger password. + Şifre gücü çok düşük. Lütfen daha güçlü bir şifre kullanın. + + + This value contains characters that are not allowed by the current restriction-level. + Bu değer, mevcut kısıtlama seviyesi tarafından izin verilmeyen karakterler içeriyor. + + + Using invisible characters is not allowed. + Görünmez karakterlerin kullanılması izin verilmez. + + + Mixing numbers from different scripts is not allowed. + Farklı yazı türlerinden sayıların karıştırılması izin verilmez. + + + Using hidden overlay characters is not allowed. + Gizli üstü kaplama karakterlerinin kullanılması izin verilmez. + diff --git a/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php b/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php index 91e67cadec67e..8bd1820fa238d 100644 --- a/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php @@ -18,17 +18,19 @@ class ConstraintValidatorTest extends TestCase { + use IcuCompatibilityTrait; + /** * @dataProvider formatValueProvider */ - public function testFormatValue($expected, $value, $format = 0) + public function testFormatValue(string $expected, $value, int $format = 0) { \Locale::setDefault('en'); $this->assertSame($expected, (new TestFormatValueConstraintValidator())->formatValueProxy($value, $format)); } - public static function formatValueProvider() + public static function formatValueProvider(): array { $defaultTimezone = date_default_timezone_get(); date_default_timezone_set('Europe/Moscow'); // GMT+3 @@ -43,10 +45,10 @@ public static function formatValueProvider() ['object', $toString = new TestToStringObject()], ['ccc', $toString, ConstraintValidator::OBJECT_TO_STRING], ['object', $dateTime = new \DateTimeImmutable('1971-02-02T08:00:00UTC')], - [class_exists(\IntlDateFormatter::class) ? 'Oct 4, 2019, 11:02 AM' : '2019-10-04 11:02:03', new \DateTimeImmutable('2019-10-04T11:02:03+09:00'), ConstraintValidator::PRETTY_DATE], - [class_exists(\IntlDateFormatter::class) ? 'Feb 2, 1971, 8:00 AM' : '1971-02-02 08:00:00', $dateTime, ConstraintValidator::PRETTY_DATE], - [class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 6:00 AM' : '1970-01-01 06:00:00', new \DateTimeImmutable('1970-01-01T06:00:00Z'), ConstraintValidator::PRETTY_DATE], - [class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 3:00 PM' : '1970-01-01 15:00:00', (new \DateTimeImmutable('1970-01-01T23:00:00'))->setTimezone(new \DateTimeZone('America/New_York')), ConstraintValidator::PRETTY_DATE], + [class_exists(\IntlDateFormatter::class) ? static::normalizeIcuSpaces("Oct 4, 2019, 11:02\u{202F}AM") : '2019-10-04 11:02:03', new \DateTimeImmutable('2019-10-04T11:02:03+09:00'), ConstraintValidator::PRETTY_DATE], + [class_exists(\IntlDateFormatter::class) ? static::normalizeIcuSpaces("Feb 2, 1971, 8:00\u{202F}AM") : '1971-02-02 08:00:00', $dateTime, ConstraintValidator::PRETTY_DATE], + [class_exists(\IntlDateFormatter::class) ? static::normalizeIcuSpaces("Jan 1, 1970, 6:00\u{202F}AM") : '1970-01-01 06:00:00', new \DateTimeImmutable('1970-01-01T06:00:00Z'), ConstraintValidator::PRETTY_DATE], + [class_exists(\IntlDateFormatter::class) ? static::normalizeIcuSpaces("Jan 1, 1970, 3:00\u{202F}PM") : '1970-01-01 15:00:00', (new \DateTimeImmutable('1970-01-01T23:00:00'))->setTimezone(new \DateTimeZone('America/New_York')), ConstraintValidator::PRETTY_DATE], ]; if (\PHP_VERSION_ID >= 80100) { @@ -65,7 +67,7 @@ public function validate($value, Constraint $constraint): void { } - public function formatValueProxy($value, $format) + public function formatValueProxy($value, int $format): string { return $this->formatValue($value, $format); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php index ea9efe90e238c..06c0edfbf1a50 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php @@ -14,12 +14,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\EqualTo; use Symfony\Component\Validator\Constraints\EqualToValidator; +use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; /** * @author Daniel Holmes */ class EqualToValidatorTest extends AbstractComparisonValidatorTestCase { + use IcuCompatibilityTrait; + protected function createValidator(): EqualToValidator { return new EqualToValidator(); @@ -61,14 +64,14 @@ public static function provideInvalidComparisons(): array return [ [1, '1', 2, '2', 'int'], ['22', '"22"', '333', '"333"', 'string'], - [new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2001-01-01 UTC'), 'Jan 1, 2001, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'], + [new \DateTime('2001-01-01'), self::normalizeIcuSpaces("Jan 1, 2001, 12:00\u{202F}AM"), new \DateTime('2000-01-01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2001-01-01'), self::normalizeIcuSpaces("Jan 1, 2001, 12:00\u{202F}AM"), '2000-01-01', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2001-01-01 UTC'), self::normalizeIcuSpaces("Jan 1, 2001, 12:00\u{202F}AM"), '2000-01-01 UTC', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], [new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], ]; } - public static function provideComparisonsToNullValueAtPropertyPath() + public static function provideComparisonsToNullValueAtPropertyPath(): array { return [ [5, '5', false], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php index e1e29576bc7b3..aaeb98d8bfd3f 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php @@ -14,12 +14,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; use Symfony\Component\Validator\Constraints\GreaterThanOrEqualValidator; +use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; /** * @author Daniel Holmes */ class GreaterThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCase { + use IcuCompatibilityTrait; + protected function createValidator(): GreaterThanOrEqualValidator { return new GreaterThanOrEqualValidator(); @@ -64,14 +67,14 @@ public static function provideInvalidComparisons(): array { return [ [1, '1', 2, '2', 'int'], - [new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2005/01/01'), 'Jan 1, 2005, 12:00 AM', 'DateTime'], - [new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', '2005/01/01', 'Jan 1, 2005, 12:00 AM', 'DateTime'], - [new \DateTime('2000/01/01 UTC'), 'Jan 1, 2000, 12:00 AM', '2005/01/01 UTC', 'Jan 1, 2005, 12:00 AM', 'DateTime'], + [new \DateTime('2000/01/01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), new \DateTime('2005/01/01'), self::normalizeIcuSpaces("Jan 1, 2005, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000/01/01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), '2005/01/01', self::normalizeIcuSpaces("Jan 1, 2005, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000/01/01 UTC'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), '2005/01/01 UTC', self::normalizeIcuSpaces("Jan 1, 2005, 12:00\u{202F}AM"), 'DateTime'], ['b', '"b"', 'c', '"c"', 'string'], ]; } - public static function provideComparisonsToNullValueAtPropertyPath() + public static function provideComparisonsToNullValueAtPropertyPath(): array { return [ [5, '5', true], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php index bc4b5df04fb06..ab8a0ac10f51d 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php @@ -14,12 +14,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GreaterThan; use Symfony\Component\Validator\Constraints\GreaterThanValidator; +use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; /** * @author Daniel Holmes */ class GreaterThanValidatorTest extends AbstractComparisonValidatorTestCase { + use IcuCompatibilityTrait; + protected function createValidator(): GreaterThanValidator { return new GreaterThanValidator(); @@ -60,12 +63,12 @@ public static function provideInvalidComparisons(): array return [ [1, '1', 2, '2', 'int'], [2, '2', 2, '2', 'int'], - [new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2005/01/01'), 'Jan 1, 2005, 12:00 AM', 'DateTime'], - [new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', '2005/01/01', 'Jan 1, 2005, 12:00 AM', 'DateTime'], - [new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', '2000/01/01', 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2000/01/01 UTC'), 'Jan 1, 2000, 12:00 AM', '2005/01/01 UTC', 'Jan 1, 2005, 12:00 AM', 'DateTime'], - [new \DateTime('2000/01/01 UTC'), 'Jan 1, 2000, 12:00 AM', '2000/01/01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'], + [new \DateTime('2000/01/01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), new \DateTime('2005/01/01'), self::normalizeIcuSpaces("Jan 1, 2005, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000/01/01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), new \DateTime('2000/01/01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000/01/01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), '2005/01/01', self::normalizeIcuSpaces("Jan 1, 2005, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000/01/01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), '2000/01/01', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000/01/01 UTC'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), '2005/01/01 UTC', self::normalizeIcuSpaces("Jan 1, 2005, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000/01/01 UTC'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), '2000/01/01 UTC', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], [new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], [new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], ['22', '"22"', '333', '"333"', 'string'], @@ -73,7 +76,7 @@ public static function provideInvalidComparisons(): array ]; } - public static function provideComparisonsToNullValueAtPropertyPath() + public static function provideComparisonsToNullValueAtPropertyPath(): array { return [ [5, '5', true], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php index 334743c4b0e5e..424c028c0a808 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php @@ -14,12 +14,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\IdenticalTo; use Symfony\Component\Validator\Constraints\IdenticalToValidator; +use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; /** * @author Daniel Holmes */ class IdenticalToValidatorTest extends AbstractComparisonValidatorTestCase { + use IcuCompatibilityTrait; + protected function createValidator(): IdenticalToValidator { return new IdenticalToValidator(); @@ -81,13 +84,13 @@ public static function provideInvalidComparisons(): array [1, '1', 2, '2', 'int'], [2, '2', '2', '"2"', 'string'], ['22', '"22"', '333', '"333"', 'string'], - [new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', 'DateTime'], - [new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', new \DateTime('1999-01-01'), 'Jan 1, 1999, 12:00 AM', 'DateTime'], + [new \DateTime('2001-01-01'), self::normalizeIcuSpaces("Jan 1, 2001, 12:00\u{202F}AM"), new \DateTime('2001-01-01'), self::normalizeIcuSpaces("Jan 1, 2001, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2001-01-01'), self::normalizeIcuSpaces("Jan 1, 2001, 12:00\u{202F}AM"), new \DateTime('1999-01-01'), self::normalizeIcuSpaces("Jan 1, 1999, 12:00\u{202F}AM"), 'DateTime'], [new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], ]; } - public static function provideComparisonsToNullValueAtPropertyPath() + public static function provideComparisonsToNullValueAtPropertyPath(): array { return [ [5, '5', false], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php index aec7edcb603e4..d4bcc346fdabf 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php @@ -14,12 +14,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\LessThanOrEqual; use Symfony\Component\Validator\Constraints\LessThanOrEqualValidator; +use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; /** * @author Daniel Holmes */ class LessThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCase { + use IcuCompatibilityTrait; + protected function createValidator(): LessThanOrEqualValidator { return new LessThanOrEqualValidator(); @@ -66,15 +69,15 @@ public static function provideInvalidComparisons(): array { return [ [2, '2', 1, '1', 'int'], - [new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2010-01-01 UTC'), 'Jan 1, 2010, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'], + [new \DateTime('2010-01-01'), self::normalizeIcuSpaces("Jan 1, 2010, 12:00\u{202F}AM"), new \DateTime('2000-01-01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2010-01-01'), self::normalizeIcuSpaces("Jan 1, 2010, 12:00\u{202F}AM"), '2000-01-01', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2010-01-01 UTC'), self::normalizeIcuSpaces("Jan 1, 2010, 12:00\u{202F}AM"), '2000-01-01 UTC', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], [new ComparisonTest_Class(5), '5', new ComparisonTest_Class(4), '4', __NAMESPACE__.'\ComparisonTest_Class'], ['c', '"c"', 'b', '"b"', 'string'], ]; } - public static function provideComparisonsToNullValueAtPropertyPath() + public static function provideComparisonsToNullValueAtPropertyPath(): array { return [ [5, '5', true], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php index 7721d812fe9a5..14064962a26d7 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php @@ -14,12 +14,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\LessThan; use Symfony\Component\Validator\Constraints\LessThanValidator; +use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; /** * @author Daniel Holmes */ class LessThanValidatorTest extends AbstractComparisonValidatorTestCase { + use IcuCompatibilityTrait; + protected function createValidator(): LessThanValidator { return new LessThanValidator(); @@ -60,19 +63,19 @@ public static function provideInvalidComparisons(): array return [ [3, '3', 2, '2', 'int'], [2, '2', 2, '2', 'int'], - [new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2010-01-01 UTC'), 'Jan 1, 2010, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2000-01-01 UTC'), 'Jan 1, 2000, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'], + [new \DateTime('2010-01-01'), self::normalizeIcuSpaces("Jan 1, 2010, 12:00\u{202F}AM"), new \DateTime('2000-01-01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000-01-01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), new \DateTime('2000-01-01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2010-01-01'), self::normalizeIcuSpaces("Jan 1, 2010, 12:00\u{202F}AM"), '2000-01-01', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000-01-01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), '2000-01-01', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2010-01-01 UTC'), self::normalizeIcuSpaces("Jan 1, 2010, 12:00\u{202F}AM"), '2000-01-01 UTC', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000-01-01 UTC'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), '2000-01-01 UTC', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], [new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], [new ComparisonTest_Class(6), '6', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], ['333', '"333"', '22', '"22"', 'string'], ]; } - public static function provideComparisonsToNullValueAtPropertyPath() + public static function provideComparisonsToNullValueAtPropertyPath(): array { return [ [5, '5', true], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php index de8249d8b37dd..a297ae37fba3f 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php @@ -14,12 +14,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\NotEqualTo; use Symfony\Component\Validator\Constraints\NotEqualToValidator; +use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; /** * @author Daniel Holmes */ class NotEqualToValidatorTest extends AbstractComparisonValidatorTestCase { + use IcuCompatibilityTrait; + protected function createValidator(): NotEqualToValidator { return new NotEqualToValidator(); @@ -61,14 +64,14 @@ public static function provideInvalidComparisons(): array [3, '3', 3, '3', 'int'], ['2', '"2"', 2, '2', 'int'], ['a', '"a"', 'a', '"a"', 'string'], - [new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'], - [new \DateTime('2000-01-01 UTC'), 'Jan 1, 2000, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'], + [new \DateTime('2000-01-01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), new \DateTime('2000-01-01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000-01-01'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), '2000-01-01', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], + [new \DateTime('2000-01-01 UTC'), self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), '2000-01-01 UTC', self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], [new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], ]; } - public static function provideComparisonsToNullValueAtPropertyPath() + public static function provideComparisonsToNullValueAtPropertyPath(): array { return [ [5, '5', true], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php index a594599c4ae09..eab5395bcd808 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php @@ -14,12 +14,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\NotIdenticalTo; use Symfony\Component\Validator\Constraints\NotIdenticalToValidator; +use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; /** * @author Daniel Holmes */ class NotIdenticalToValidatorTest extends AbstractComparisonValidatorTestCase { + use IcuCompatibilityTrait; + protected function createValidator(): NotIdenticalToValidator { return new NotIdenticalToValidator(); @@ -77,17 +80,15 @@ public static function provideInvalidComparisons(): array $date = new \DateTime('2000-01-01'); $object = new ComparisonTest_Class(2); - $comparisons = [ + return [ [3, '3', 3, '3', 'int'], ['a', '"a"', 'a', '"a"', 'string'], - [$date, 'Jan 1, 2000, 12:00 AM', $date, 'Jan 1, 2000, 12:00 AM', 'DateTime'], + [$date, self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), $date, self::normalizeIcuSpaces("Jan 1, 2000, 12:00\u{202F}AM"), 'DateTime'], [$object, '2', $object, '2', __NAMESPACE__.'\ComparisonTest_Class'], ]; - - return $comparisons; } - public static function provideComparisonsToNullValueAtPropertyPath() + public static function provideComparisonsToNullValueAtPropertyPath(): array { return [ [5, '5', true], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/RangeValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/RangeValidatorTest.php index fffeeaffb4f5c..876595c37fc94 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/RangeValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/RangeValidatorTest.php @@ -16,9 +16,12 @@ use Symfony\Component\Validator\Constraints\RangeValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; class RangeValidatorTest extends ConstraintValidatorTestCase { + use IcuCompatibilityTrait; + protected function createValidator(): RangeValidator { return new RangeValidator(); @@ -31,7 +34,7 @@ public function testNullIsValid() $this->assertNoViolation(); } - public static function getTenToTwenty() + public static function getTenToTwenty(): array { return [ [10.00001], @@ -55,7 +58,7 @@ public static function getLessThanTen() ]; } - public static function getMoreThanTwenty() + public static function getMoreThanTwenty(): array { return [ [20.000001, '20.000001'], @@ -277,7 +280,7 @@ public function testInvalidValuesCombinedMinNamed($value, $formattedValue) ->assertRaised(); } - public static function getTenthToTwentiethMarch2014() + public static function getTenthToTwentiethMarch2014(): array { // The provider runs before setUp(), so we need to manually fix // the default timezone @@ -288,18 +291,17 @@ public static function getTenthToTwentiethMarch2014() [new \DateTime('March 10, 2014')], [new \DateTime('March 15, 2014')], [new \DateTime('March 20, 2014')], + [new \DateTimeImmutable('March 10, 2014')], + [new \DateTimeImmutable('March 15, 2014')], + [new \DateTimeImmutable('March 20, 2014')], ]; - $tests[] = [new \DateTimeImmutable('March 10, 2014')]; - $tests[] = [new \DateTimeImmutable('March 15, 2014')]; - $tests[] = [new \DateTimeImmutable('March 20, 2014')]; - date_default_timezone_set($timezone); return $tests; } - public static function getSoonerThanTenthMarch2014() + public static function getSoonerThanTenthMarch2014(): array { // The provider runs before setUp(), so we need to manually fix // the default timezone @@ -307,19 +309,18 @@ public static function getSoonerThanTenthMarch2014() date_default_timezone_set('UTC'); $tests = [ - [new \DateTime('March 20, 2013'), 'Mar 20, 2013, 12:00 AM'], - [new \DateTime('March 9, 2014'), 'Mar 9, 2014, 12:00 AM'], + [new \DateTime('March 20, 2013'), self::normalizeIcuSpaces("Mar 20, 2013, 12:00\u{202F}AM")], + [new \DateTime('March 9, 2014'), self::normalizeIcuSpaces("Mar 9, 2014, 12:00\u{202F}AM")], + [new \DateTimeImmutable('March 20, 2013'), self::normalizeIcuSpaces("Mar 20, 2013, 12:00\u{202F}AM")], + [new \DateTimeImmutable('March 9, 2014'), self::normalizeIcuSpaces("Mar 9, 2014, 12:00\u{202F}AM")], ]; - $tests[] = [new \DateTimeImmutable('March 20, 2013'), 'Mar 20, 2013, 12:00 AM']; - $tests[] = [new \DateTimeImmutable('March 9, 2014'), 'Mar 9, 2014, 12:00 AM']; - date_default_timezone_set($timezone); return $tests; } - public static function getLaterThanTwentiethMarch2014() + public static function getLaterThanTwentiethMarch2014(): array { // The provider runs before setUp(), so we need to manually fix // the default timezone @@ -327,13 +328,12 @@ public static function getLaterThanTwentiethMarch2014() date_default_timezone_set('UTC'); $tests = [ - [new \DateTime('March 21, 2014'), 'Mar 21, 2014, 12:00 AM'], - [new \DateTime('March 9, 2015'), 'Mar 9, 2015, 12:00 AM'], + [new \DateTime('March 21, 2014'), self::normalizeIcuSpaces("Mar 21, 2014, 12:00\u{202F}AM")], + [new \DateTime('March 9, 2015'), self::normalizeIcuSpaces("Mar 9, 2015, 12:00\u{202F}AM")], + [new \DateTimeImmutable('March 21, 2014'), self::normalizeIcuSpaces("Mar 21, 2014, 12:00\u{202F}AM")], + [new \DateTimeImmutable('March 9, 2015'), self::normalizeIcuSpaces("Mar 9, 2015, 12:00\u{202F}AM")], ]; - $tests[] = [new \DateTimeImmutable('March 21, 2014'), 'Mar 21, 2014, 12:00 AM']; - $tests[] = [new \DateTimeImmutable('March 9, 2015'), 'Mar 9, 2015, 12:00 AM']; - date_default_timezone_set($timezone); return $tests; @@ -375,7 +375,7 @@ public function testValidDatesMinMax($value) /** * @dataProvider getSoonerThanTenthMarch2014 */ - public function testInvalidDatesMin($value, $dateTimeAsString) + public function testInvalidDatesMin(\DateTimeInterface $value, string $dateTimeAsString) { // Conversion of dates to string differs between ICU versions // Make sure we have the correct version loaded @@ -390,7 +390,7 @@ public function testInvalidDatesMin($value, $dateTimeAsString) $this->buildViolation('myMessage') ->setParameter('{{ value }}', $dateTimeAsString) - ->setParameter('{{ limit }}', 'Mar 10, 2014, 12:00 AM') + ->setParameter('{{ limit }}', self::normalizeIcuSpaces("Mar 10, 2014, 12:00\u{202F}AM")) ->setCode(Range::TOO_LOW_ERROR) ->assertRaised(); } @@ -398,7 +398,7 @@ public function testInvalidDatesMin($value, $dateTimeAsString) /** * @dataProvider getLaterThanTwentiethMarch2014 */ - public function testInvalidDatesMax($value, $dateTimeAsString) + public function testInvalidDatesMax(\DateTimeInterface $value, string $dateTimeAsString) { // Conversion of dates to string differs between ICU versions // Make sure we have the correct version loaded @@ -413,7 +413,7 @@ public function testInvalidDatesMax($value, $dateTimeAsString) $this->buildViolation('myMessage') ->setParameter('{{ value }}', $dateTimeAsString) - ->setParameter('{{ limit }}', 'Mar 20, 2014, 12:00 AM') + ->setParameter('{{ limit }}', self::normalizeIcuSpaces("Mar 20, 2014, 12:00\u{202F}AM")) ->setCode(Range::TOO_HIGH_ERROR) ->assertRaised(); } @@ -421,7 +421,7 @@ public function testInvalidDatesMax($value, $dateTimeAsString) /** * @dataProvider getLaterThanTwentiethMarch2014 */ - public function testInvalidDatesCombinedMax($value, $dateTimeAsString) + public function testInvalidDatesCombinedMax(\DateTimeInterface $value, string $dateTimeAsString) { // Conversion of dates to string differs between ICU versions // Make sure we have the correct version loaded @@ -437,8 +437,8 @@ public function testInvalidDatesCombinedMax($value, $dateTimeAsString) $this->buildViolation('myNotInRangeMessage') ->setParameter('{{ value }}', $dateTimeAsString) - ->setParameter('{{ min }}', 'Mar 10, 2014, 12:00 AM') - ->setParameter('{{ max }}', 'Mar 20, 2014, 12:00 AM') + ->setParameter('{{ min }}', self::normalizeIcuSpaces("Mar 10, 2014, 12:00\u{202F}AM")) + ->setParameter('{{ max }}', self::normalizeIcuSpaces("Mar 20, 2014, 12:00\u{202F}AM")) ->setCode(Range::NOT_IN_RANGE_ERROR) ->assertRaised(); } @@ -462,13 +462,13 @@ public function testInvalidDatesCombinedMin($value, $dateTimeAsString) $this->buildViolation('myNotInRangeMessage') ->setParameter('{{ value }}', $dateTimeAsString) - ->setParameter('{{ min }}', 'Mar 10, 2014, 12:00 AM') - ->setParameter('{{ max }}', 'Mar 20, 2014, 12:00 AM') + ->setParameter('{{ min }}', self::normalizeIcuSpaces("Mar 10, 2014, 12:00\u{202F}AM")) + ->setParameter('{{ max }}', self::normalizeIcuSpaces("Mar 20, 2014, 12:00\u{202F}AM")) ->setCode(Range::NOT_IN_RANGE_ERROR) ->assertRaised(); } - public function getInvalidValues() + public function getInvalidValues(): array { return [ [9.999999], @@ -930,7 +930,7 @@ public function testInvalidDatesMinPropertyPath($value, $dateTimeAsString) $this->buildViolation('myMessage') ->setParameter('{{ value }}', $dateTimeAsString) - ->setParameter('{{ limit }}', 'Mar 10, 2014, 12:00 AM') + ->setParameter('{{ limit }}', self::normalizeIcuSpaces("Mar 10, 2014, 12:00\u{202F}AM")) ->setParameter('{{ min_limit_path }}', 'value') ->setCode(Range::TOO_LOW_ERROR) ->assertRaised(); @@ -956,7 +956,7 @@ public function testInvalidDatesMaxPropertyPath($value, $dateTimeAsString) $this->buildViolation('myMessage') ->setParameter('{{ value }}', $dateTimeAsString) - ->setParameter('{{ limit }}', 'Mar 20, 2014, 12:00 AM') + ->setParameter('{{ limit }}', self::normalizeIcuSpaces("Mar 20, 2014, 12:00\u{202F}AM")) ->setParameter('{{ max_limit_path }}', 'value') ->setCode(Range::TOO_HIGH_ERROR) ->assertRaised(); @@ -983,8 +983,8 @@ public function testInvalidDatesCombinedMaxPropertyPath($value, $dateTimeAsStrin $this->buildViolation('myNotInRangeMessage') ->setParameter('{{ value }}', $dateTimeAsString) - ->setParameter('{{ min }}', 'Mar 10, 2014, 12:00 AM') - ->setParameter('{{ max }}', 'Mar 20, 2014, 12:00 AM') + ->setParameter('{{ min }}', self::normalizeIcuSpaces("Mar 10, 2014, 12:00\u{202F}AM")) + ->setParameter('{{ max }}', self::normalizeIcuSpaces("Mar 20, 2014, 12:00\u{202F}AM")) ->setParameter('{{ max_limit_path }}', 'max') ->setParameter('{{ min_limit_path }}', 'min') ->setCode(Range::NOT_IN_RANGE_ERROR) @@ -1012,8 +1012,8 @@ public function testInvalidDatesCombinedMinPropertyPath($value, $dateTimeAsStrin $this->buildViolation('myNotInRangeMessage') ->setParameter('{{ value }}', $dateTimeAsString) - ->setParameter('{{ min }}', 'Mar 10, 2014, 12:00 AM') - ->setParameter('{{ max }}', 'Mar 20, 2014, 12:00 AM') + ->setParameter('{{ min }}', self::normalizeIcuSpaces("Mar 10, 2014, 12:00\u{202F}AM")) + ->setParameter('{{ max }}', self::normalizeIcuSpaces("Mar 20, 2014, 12:00\u{202F}AM")) ->setParameter('{{ max_limit_path }}', 'max') ->setParameter('{{ min_limit_path }}', 'min') ->setCode(Range::NOT_IN_RANGE_ERROR) diff --git a/src/Symfony/Component/Validator/Tests/IcuCompatibilityTrait.php b/src/Symfony/Component/Validator/Tests/IcuCompatibilityTrait.php new file mode 100644 index 0000000000000..bdd83feb9a309 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/IcuCompatibilityTrait.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests; + +trait IcuCompatibilityTrait +{ + /** + * Normalized spaces in date strings generated by INTL for older ICU versions. + * + * In version 72.1, ICU started to render a narrow non-breaking space (NNBSP) into localized time strings. This + * method allows us to write expectations in a forward-compatible manner. + */ + private static function normalizeIcuSpaces(string $input): string + { + if (\defined('INTL_ICU_VERSION') && version_compare(\INTL_ICU_VERSION, '72.1', '>=')) { + return $input; + } + + return str_replace("\u{202F}", ' ', $input); + } +} diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index e7ee44b09f617..13fb6cd34c05a 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -159,11 +159,11 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount $n = substr($n, 1 + $i); } if (null !== $sleep) { - if (!isset($sleep[$n]) || ($i && $c !== $class)) { + if (!isset($sleep[$name]) && (!isset($sleep[$n]) || ($i && $c !== $class))) { unset($arrayValue[$name]); continue; } - $sleep[$n] = false; + unset($sleep[$name], $sleep[$n]); } if (!\array_key_exists($name, $proto) || $proto[$name] !== $v || "\x00Error\x00trace" === $name || "\x00Exception\x00trace" === $name) { $properties[$c][$n] = $v; @@ -171,9 +171,7 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount } if ($sleep) { foreach ($sleep as $n => $v) { - if (false !== $v) { - trigger_error(sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $n), \E_USER_NOTICE); - } + trigger_error(sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $n), \E_USER_NOTICE); } } if (method_exists($class, '__unserialize')) { diff --git a/src/Symfony/Component/VarExporter/Internal/Hydrator.php b/src/Symfony/Component/VarExporter/Internal/Hydrator.php index f665f6ee15c6e..ebc5914326386 100644 --- a/src/Symfony/Component/VarExporter/Internal/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Internal/Hydrator.php @@ -271,10 +271,10 @@ public static function getPropertyScopes($class) $name = $property->name; if (\ReflectionProperty::IS_PRIVATE & $flags) { - $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $class : null]; + $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $class : null, $property]; continue; } - $propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $property->class : null]; + $propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $property->class : null, $property]; if (\ReflectionProperty::IS_PROTECTED & $flags) { $propertyScopes["\0*\0$name"] = $propertyScopes[$name]; @@ -288,8 +288,8 @@ public static function getPropertyScopes($class) if (!$property->isStatic()) { $name = $property->name; $readonlyScope = $property->isReadOnly() ? $class : null; - $propertyScopes["\0$class\0$name"] = [$class, $name, $readonlyScope]; - $propertyScopes[$name] ??= [$class, $name, $readonlyScope]; + $propertyScopes["\0$class\0$name"] = [$class, $name, $readonlyScope, $property]; + $propertyScopes[$name] ??= [$class, $name, $readonlyScope, $property]; } } } diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php index 2f649dd1ca481..f47dea4d8e6f5 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php @@ -65,29 +65,32 @@ public function initialize($instance, $propertyName, $propertyScope) return $this->status = self::STATUS_INITIALIZED_PARTIAL; } - $status = self::STATUS_UNINITIALIZED_PARTIAL; - if ($initializer = $this->initializer["\0"] ?? null) { if (!\is_array($values = $initializer($instance, LazyObjectRegistry::$defaultProperties[$class]))) { throw new \TypeError(sprintf('The lazy-initializer defined for instance of "%s" must return an array, got "%s".', $class, get_debug_type($values))); } $properties = (array) $instance; foreach ($values as $key => $value) { - if ($k === $key) { - $status = self::STATUS_INITIALIZED_PARTIAL; - } if (!\array_key_exists($key, $properties) && [$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) { $scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class); $accessor = LazyObjectRegistry::$classAccessors[$scope] ??= LazyObjectRegistry::getClassAccessors($scope); $accessor['set']($instance, $name, $value); + + if ($k === $key) { + $this->status = self::STATUS_INITIALIZED_PARTIAL; + } } } } - return $status; + return $this->status; + } + + if (self::STATUS_INITIALIZED_PARTIAL === $this->status) { + return self::STATUS_INITIALIZED_PARTIAL; } - $this->status = self::STATUS_INITIALIZED_FULL; + $this->status = self::STATUS_INITIALIZED_PARTIAL; try { if ($defaultProperties = array_diff_key(LazyObjectRegistry::$defaultProperties[$instance::class], $this->skippedProperties)) { @@ -102,7 +105,7 @@ public function initialize($instance, $propertyName, $propertyScope) throw $e; } - return self::STATUS_INITIALIZED_FULL; + return $this->status = self::STATUS_INITIALIZED_FULL; } public function reset($instance): void diff --git a/src/Symfony/Component/VarExporter/LazyGhostTrait.php b/src/Symfony/Component/VarExporter/LazyGhostTrait.php index 13e33f59c9bd8..cd2dc71da879c 100644 --- a/src/Symfony/Component/VarExporter/LazyGhostTrait.php +++ b/src/Symfony/Component/VarExporter/LazyGhostTrait.php @@ -172,10 +172,18 @@ public function &__get($name): mixed $scope = Registry::getScope($propertyScopes, $class, $name); $state = $this->lazyObjectState ?? null; - if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"])) - && LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope) - ) { - goto get_in_scope; + if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))) { + if (LazyObjectState::STATUS_INITIALIZED_FULL === $state->status) { + // Work around php/php-src#12695 + $property = $propertyScopes[null === $scope ? $name : "\0$scope\0$name"][3] + ?? (Hydrator::$propertyScopes[$this::class] = Hydrator::getPropertyScopes($this::class))[3]; + } else { + $property = null; + } + + if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope)) { + goto get_in_scope; + } } } @@ -237,7 +245,9 @@ public function __set($name, $value): void $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); $state = $this->lazyObjectState ?? null; - if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))) { + if ($state && ($readonlyScope === $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); } @@ -271,6 +281,7 @@ public function __isset($name): bool $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) ) { goto isset_in_scope; @@ -300,7 +311,9 @@ public function __unset($name): void $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); $state = $this->lazyObjectState ?? null; - if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))) { + if ($state && ($readonlyScope === $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); } @@ -355,7 +368,7 @@ public function __serialize(): array $data = []; foreach (parent::__sleep() as $name) { - $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0$scope\0$name"] ?? $k = null; + $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0$class\0$name"] ?? $properties[$k = "\0$scope\0$name"] ?? $k = null; if (null === $k) { trigger_error(sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $name), \E_USER_NOTICE); diff --git a/src/Symfony/Component/VarExporter/LazyProxyTrait.php b/src/Symfony/Component/VarExporter/LazyProxyTrait.php index 153c3820844b5..d683ec3f1259d 100644 --- a/src/Symfony/Component/VarExporter/LazyProxyTrait.php +++ b/src/Symfony/Component/VarExporter/LazyProxyTrait.php @@ -295,7 +295,7 @@ public function __serialize(): array $data = []; foreach (parent::__sleep() as $name) { - $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0$scope\0$name"] ?? $k = null; + $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0$class\0$name"] ?? $properties[$k = "\0$scope\0$name"] ?? $k = null; if (null === $k) { trigger_error(sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $name), \E_USER_NOTICE); diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index 155715de662c9..71530fd55d7f8 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -322,6 +322,9 @@ private static function exportPropertyScopes(string $parent): string { $propertyScopes = Hydrator::$propertyScopes[$parent] ??= Hydrator::getPropertyScopes($parent); uksort($propertyScopes, 'strnatcmp'); + foreach ($propertyScopes as $k => $v) { + unset($propertyScopes[$k][3]); + } $propertyScopes = VarExporter::export($propertyScopes); $propertyScopes = str_replace(VarExporter::export($parent), 'parent::class', $propertyScopes); $propertyScopes = preg_replace("/(?|(,)\n( ) |\n |,\n (\]))/", '$1$2', $propertyScopes); diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/var-on-sleep.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/var-on-sleep.php index 9fd44bd59092d..a0d7e3f8cb21e 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/var-on-sleep.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/var-on-sleep.php @@ -11,6 +11,14 @@ 'night', ], ], + 'Symfony\\Component\\VarExporter\\Tests\\GoodNight' => [ + 'foo' => [ + 'afternoon', + ], + 'bar' => [ + 'morning', + ], + ], ], $o[0], [] diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index 0cbbd835b8f64..9e47ebcf98642 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -65,8 +65,10 @@ public function testUnsetPublic() $this->assertSame(["\0".TestClass::class."\0lazyObjectState"], array_keys((array) $instance)); unset($instance->public); - $this->assertFalse(isset($instance->public)); $this->assertSame(4, $instance->publicReadonly); + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('__isset(public)'); + isset($instance->public); } public function testSetPublic() @@ -146,7 +148,13 @@ public function testMagicClass(MagicClass $instance) $instance->bar = 123; $serialized = serialize($instance); $clone = unserialize($serialized); - $this->assertSame(123, $clone->bar); + + if ($instance instanceof ChildMagicClass) { + // ChildMagicClass redefines the $data property but not the __sleep() method + $this->assertFalse(isset($clone->bar)); + } else { + $this->assertSame(123, $clone->bar); + } } public static function provideMagicClass() diff --git a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php index 8081ddded2411..41eef4fb7a032 100644 --- a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php +++ b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php @@ -333,17 +333,21 @@ public function setFlags($flags): void class GoodNight { public $good; + protected $foo; + private $bar; public function __construct() { unset($this->good); + $this->foo = 'afternoon'; + $this->bar = 'morning'; } public function __sleep(): array { $this->good = 'night'; - return ['good']; + return ['good', 'foo', "\0*\0foo", "\0".__CLASS__."\0bar"]; } } diff --git a/src/Symfony/Component/VarExporter/VarExporter.php b/src/Symfony/Component/VarExporter/VarExporter.php index c12eb4f956672..b5ce3ae9e4633 100644 --- a/src/Symfony/Component/VarExporter/VarExporter.php +++ b/src/Symfony/Component/VarExporter/VarExporter.php @@ -82,7 +82,7 @@ public static function export(mixed $value, bool &$isStaticValue = null, array & ksort($states); $wakeups = [null]; - foreach ($states as $k => $v) { + foreach ($states as $v) { if (\is_array($v)) { $wakeups[-$v[0]] = $v[1]; } else {