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 { pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy