diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 0df7764a3ed29..575f27685e010 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -215,10 +215,14 @@ jobs: # sudo rm -rf .phpunit # [ -d .phpunit.bak ] && mv .phpunit.bak .phpunit - - uses: marceloprado/has-changed-path@v1.0.1 + - name: Check for changes in translation files id: changed-translation-files - with: - paths: src/**/Resources/translations/*.xlf + run: | + if git diff --quiet HEAD~1 HEAD -- 'src/**/Resources/translations/*.xlf'; then + echo "{changed}={true}" >> $GITHUB_OUTPUT + else + echo "{changed}={false}" >> $GITHUB_OUTPUT + fi - name: Check Translation Status if: steps.changed-translation-files.outputs.changed == 'true' diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 33b87c42af2de..f90caa71d3d8a 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -32,6 +32,7 @@ jobs: - php: '8.2' mode: low-deps - php: '8.3' + - php: '8.4' #mode: experimental fail-fast: false @@ -59,7 +60,7 @@ jobs: git config --global init.defaultBranch main git config --global advice.detachedHead false - (php --ri relay 2>&1 > /dev/null) || sudo rm /etc/php/*/cli/conf.d/20-relay.ini + (php --ri relay 2>&1 > /dev/null) || sudo rm -f /etc/php/*/cli/conf.d/20-relay.ini COMPOSER_HOME="$(composer config home)" ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" diff --git a/CHANGELOG-6.4.md b/CHANGELOG-6.4.md index 723085c67be52..2e0f70ad13e6a 100644 --- a/CHANGELOG-6.4.md +++ b/CHANGELOG-6.4.md @@ -7,6 +7,41 @@ in 6.4 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.4.0...v6.4.1 +* 6.4.6 (2024-04-03) + + * bug #54400 [HttpClient] stop all server processes after tests have run (xabbuh) + * bug #54435 [Console] respect multi-byte characters when rendering vertical-style tables (xabbuh) + * bug #54419 Fix TypeError on ProgressBar (Fan2Shrek) + * bug #54425 [TwigBridge] Remove whitespaces from block form_help output (rosier) + * bug #53969 [Mailer] include message id provided by the MTA when dispatching the `SentMessageEvent` (xabbuh) + * bug #54315 [Serializer] Fixed BackedEnumNormalizer priority for translatable enum (IndraGunawan) + * bug #54372 [Config] Fix `YamlReferenceDumper` handling of array examples (MatTheCat) + * bug #54362 [Filesystem] preserve the file modification time when mirroring directories (xabbuh) + * bug #54121 [Messenger] Catch TableNotFoundException in MySQL delete (acbramley) + * bug #54271 [DoctrineBridge] Fix deprecation warning with ORM 3 when guessing field lengths (eltharin) + * bug #54306 Throw TransformationFailedException when there is a null bytes injection (sormes) + * bug #54148 [Serializer] Fix object normalizer when properties has the same name as their accessor (NeilPeyssard) + * bug #54305 [Cache][Lock] Identify missing table in pgsql correctly and address failing integration tests (arifszn) + * bug #54199 [Mailer] [Brevo] Check that tags is present in payload before calling setTags (palgalik) + * bug #54292 [FrameworkBundle] Fix mailer config with XML (lyrixx) + * bug #54298 [Filesystem] Fix str_contains deprecation (NeilPeyssard) + * bug #54248 [Security] Correctly initialize the voter property (aschempp) + * bug #54273 [DependencyInjection] fix XmlDumper when a tag contains also a 'name' property (lyrixx) + * bug #54191 [Validator] add missing invalid extension error entry (xabbuh) + * bug #54194 [PropertyAccess] Fix checking for missing properties (nicolas-grekas) + * bug #54201 [Lock] Check the correct SQLSTATE error code for MySQL (edomato) + * bug #54252 [Lock] compatiblity with redis cluster 7 (bastnic) + * bug #54124 [Messenger] trigger retry logic when message is a redelivery (nikophil) + * bug #54254 [HttpKernel] Fix creating `ReflectionMethod` with only one argument (alexandre-daubois) + * bug #54219 [Validator] Allow BICs’ first four characters to be digits (MatTheCat) + * bug #54239 [Mailer] Fix sendmail transport not handling failure (aboks) + * bug #54207 [HttpClient] Lazily initialize CurlClientState (arjenm) + * bug #53865 [Workflow]Fix Marking when it must contains more than one tokens (lyrixx) + * bug #54137 [Validator] UniqueValidator - normalize before reducing keys (Brajk19) + * bug #54187 [FrameworkBundle] Fix PHP 8.4 deprecation on `ReflectionMethod` (alexandre-daubois) + * bug #54167 [Messenger] [Amqp] Handle AMQPConnectionException when publishing a message. (jwage) + * bug #54146 [HttpClient] Preserve float in JsonMockResponse (Jibbarth) + * 6.4.5 (2024-03-04) * bug #54113 [AssetMapper] Throw exception in Javascript compiler when PCRE error (smnandre) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c4a195380c0a3..f676651321bef 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -80,11 +80,11 @@ The Symfony Connect username in parenthesis allows to get more information - Mathieu Santostefano (welcomattic) - Alexander Schranz (alexander-schranz) - Vasilij Duško (staff) + - Simon André (simonandre) - Sarah Khalil (saro0h) - Laurent VOULLEMIER (lvo) - Konstantin Kudryashov (everzet) - Tomasz Kowalczyk (thunderer) - - Simon André (simonandre) - Guilhem N (guilhemn) - Bilal Amarni (bamarni) - Eriksen Costa @@ -97,10 +97,10 @@ The Symfony Connect username in parenthesis allows to get more information - David Buchmann (dbu) - Andrej Hudec (pulzarraider) - Jáchym Toušek (enumag) + - Ruud Kamphuis (ruudk) - Christian Raue - Eric Clemmons (ericclemmons) - Denis (yethee) - - Ruud Kamphuis (ruudk) - Michel Weimerskirch (mweimerskirch) - Issei Murasawa (issei_m) - Douglas Greenshields (shieldo) @@ -129,8 +129,8 @@ The Symfony Connect username in parenthesis allows to get more information - Bart van den Burg (burgov) - Vasilij Dusko | CREATION - Jordan Alliot (jalliot) - - John Wards (johnwards) - Phil E. Taylor (philetaylor) + - John Wards (johnwards) - Théo FIDRY - Antoine Hérault (herzult) - Konstantin.Myakshin @@ -177,6 +177,7 @@ The Symfony Connect username in parenthesis allows to get more information - Maxime Helias (maxhelias) - Robert Schönthal (digitalkaoz) - Smaine Milianni (ismail1432) + - Michael Babker (mbabker) - François-Xavier de Guillebon (de-gui_f) - Maximilian Beckers (maxbeckers) - noniagriconomie @@ -188,7 +189,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jhonny Lidfors (jhonne) - Juti Noppornpitak (shiroyuki) - Gregor Harlan (gharlan) - - Michael Babker (mbabker) - Anthony MARTIN - Sebastian Hörl (blogsh) - Tigran Azatyan (tigranazatyan) @@ -231,6 +231,7 @@ The Symfony Connect username in parenthesis allows to get more information - George Mponos (gmponos) - Richard Shank (iampersistent) - Thomas Landauer (thomas-landauer) + - Roland Franssen :) - Romain Monteil (ker0x) - Sergey (upyx) - Marco Pivetta (ocramius) @@ -257,7 +258,6 @@ The Symfony Connect username in parenthesis allows to get more information - Artur Kotyrba - Wouter J - Tyson Andre - - Roland Franssen :) - GDIBass - Samuel NELA (snela) - Vincent AUBERT (vincent) @@ -733,6 +733,7 @@ The Symfony Connect username in parenthesis allows to get more information - Vadim Borodavko (javer) - Tavo Nieves J (tavoniievez) - Luc Vieillescazes (iamluc) + - Stiven Llupa (sllupa) - Erik Saunier (snickers) - François Dume (franek) - Jerzy Lekowski (jlekowski) @@ -1043,7 +1044,6 @@ The Symfony Connect username in parenthesis allows to get more information - Florian Pfitzer (marmelatze) - Ivan Grigoriev (greedyivan) - Johann Saunier (prophet777) - - Stiven Llupa (sllupa) - Kevin SCHNEKENBURGER - Fabien Salles (blacked) - Andreas Erhard (andaris) @@ -2117,6 +2117,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sébastien HOUZÉ - Mbechezi Nawo - wivaku + - Markus Reinhold - Jingyu Wang - steveYeah - Asrorbek (asrorbek) @@ -2523,6 +2524,7 @@ The Symfony Connect username in parenthesis allows to get more information - Roy de Vos Burchart - John Stevenson - everyx + - Richard Heine - Francisco Facioni (fran6co) - Stanislav Gamaiunov (happyproff) - Iwan van Staveren (istaveren) @@ -3193,6 +3195,7 @@ The Symfony Connect username in parenthesis allows to get more information - Daniele Cesarini (ijanki) - Ismail Asci (ismailasci) - Jeffrey Moelands (jeffreymoelands) + - Jakub Caban (lustmored) - Ondřej Mirtes (mirtes) - Paulius Jarmalavičius (pjarmalavicius) - Ramon Ornelas (ramonornela) @@ -3615,6 +3618,7 @@ The Symfony Connect username in parenthesis allows to get more information - Konrad - Kovacs Nicolas - eminjk + - Gálik Pál - craigmarvelley - Stano Turza - Antoine Leblanc diff --git a/composer.json b/composer.json index 0091db8b543c7..eae0d1cafe807 100644 --- a/composer.json +++ b/composer.json @@ -137,7 +137,7 @@ "doctrine/orm": "^2.15|^3", "dragonmantank/cron-expression": "^3.1", "egulias/email-validator": "^2.1.10|^3.1|^4", - "guzzlehttp/promises": "^1.4", + "guzzlehttp/promises": "^1.4|^2.0", "league/html-to-markdown": "^5.0", "league/uri": "^6.5|^7.0", "masterminds/html5": "^2.7.2", diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php index d277992bd5a1c..0537946986d21 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -14,6 +14,7 @@ use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\FieldMapping; use Doctrine\ORM\Mapping\JoinColumnMapping; use Doctrine\ORM\Mapping\MappingException as LegacyMappingException; use Doctrine\Persistence\ManagerRegistry; @@ -129,8 +130,10 @@ public function guessMaxLength(string $class, string $property): ?ValueGuess if ($ret && isset($ret[0]->fieldMappings[$property]) && !$ret[0]->hasAssociation($property)) { $mapping = $ret[0]->getFieldMapping($property); - if (isset($mapping['length'])) { - return new ValueGuess($mapping['length'], Guess::HIGH_CONFIDENCE); + $length = $mapping instanceof FieldMapping ? $mapping->length : ($mapping['length'] ?? null); + + if (null !== $length) { + return new ValueGuess($length, Guess::HIGH_CONFIDENCE); } if (\in_array($ret[0]->getTypeOfField($property), [Types::DECIMAL, Types::FLOAT])) { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 783cf5a5524e9..92e750929f41e 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -1314,19 +1314,6 @@ public function testPassIdAndNameToView() $this->assertEquals('name', $view->vars['full_name']); } - public function testStripLeadingUnderscoresAndDigitsFromId() - { - $view = $this->factory->createNamed('_09name', static::TESTED_TYPE, null, [ - 'em' => 'default', - 'class' => self::SINGLE_IDENT_CLASS, - ]) - ->createView(); - - $this->assertEquals('name', $view->vars['id']); - $this->assertEquals('_09name', $view->vars['name']); - $this->assertEquals('_09name', $view->vars['full_name']); - } - public function testPassIdAndNameToViewWithParent() { $view = $this->factory->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE) diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 7a12292c65468..c5412dd71ba58 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -30,7 +30,7 @@ "symfony/dependency-injection": "^6.2|^7.0", "symfony/doctrine-messenger": "^5.4|^6.0|^7.0", "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/form": "^5.4.21|^6.2.7|^7.0", + "symfony/form": "^5.4.38|^6.4.6|^7.0.6", "symfony/http-kernel": "^6.3|^7.0", "symfony/lock": "^6.3|^7.0", "symfony/messenger": "^5.4|^6.0|^7.0", @@ -55,7 +55,7 @@ "doctrine/orm": "<2.15", "symfony/cache": "<5.4", "symfony/dependency-injection": "<6.2", - "symfony/form": "<5.4.21|>=6,<6.2.7", + "symfony/form": "<5.4.38|>=6,<6.4.6|>=7,<7.0.6", "symfony/http-foundation": "<6.3", "symfony/http-kernel": "<6.2", "symfony/lock": "<6.3", diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php index f660a8060f962..a2259fc1304ec 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php @@ -525,7 +525,7 @@ public function testBaselineFileWriteError() $this->expectException(\ErrorException::class); $this->expectExceptionMessageMatches('/[Ff]ailed to open stream: Permission denied/'); - set_error_handler(static function (int $errno, string $errstr, string $errfile = null, int $errline = null): bool { + set_error_handler(static function (int $errno, string $errstr, ?string $errfile = null, ?int $errline = null): bool { if ($errno & (E_WARNING | E_WARNING)) { throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); } diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures/proxy-implem.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures/proxy-implem.php index 19a9bdd5125d3..c12f1150b6986 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures/proxy-implem.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures/proxy-implem.php @@ -200,7 +200,7 @@ public function __wakeup() { } - public function setProxyInitializer(\Closure $initializer = null)%S + public function setProxyInitializer(%S\Closure $initializer = null)%S { $this->initializer%s = $initializer; } diff --git a/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/ServerRequest.php b/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/ServerRequest.php index 0bd7d5918983b..99b7abbee3f1b 100644 --- a/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/ServerRequest.php +++ b/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/ServerRequest.php @@ -26,10 +26,10 @@ class ServerRequest extends Message implements ServerRequestInterface public function __construct( string $version = '1.1', array $headers = [], - StreamInterface $body = null, + ?StreamInterface $body = null, private readonly string $requestTarget = '/', private readonly string $method = 'GET', - UriInterface|string $uri = null, + UriInterface|string|null $uri = null, private readonly array $server = [], private readonly array $cookies = [], private readonly array $query = [], diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig index cb5a60e079861..17b28fc9ab8d6 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig @@ -361,12 +361,12 @@ {# Help #} {%- block form_help -%} - {% set row_class = row_attr.class|default('') %} - {% set help_class = ' form-text' %} - {% if 'input-group' in row_class %} + {%- set row_class = row_attr.class|default('') -%} + {%- set help_class = ' form-text' -%} + {%- if 'input-group' in row_class -%} {#- Hack to properly display help with input group -#} - {% set help_class = ' input-group-text' %} - {% endif %} + {%- set help_class = ' input-group-text' -%} + {%- endif -%} {%- if help is not empty -%} {%- set help_attr = help_attr|merge({class: (help_attr.class|default('') ~ help_class ~ ' mb-0')|trim}) -%} {%- endif -%} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index 40880df4760ac..179f8e981dd32 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -154,7 +154,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($this->isNfs($realBuildDir)) { - $io->note('For better performances, you should move the cache and log directories to a non-shared folder of the VM.'); + $io->note('For better performance, you should move the cache and log directories to a non-shared folder of the VM.'); $fs->remove($realBuildDir); } else { $fs->rename($realBuildDir, $oldBuildDir); diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php index a71bf3a7610ec..b57bc0cdbec6b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php @@ -597,7 +597,7 @@ private function formatControllerLink(mixed $controller, string $anchorText, ?ca } elseif (!\is_string($controller)) { return $anchorText; } elseif (str_contains($controller, '::')) { - $r = new \ReflectionMethod($controller); + $r = new \ReflectionMethod(...explode('::', $controller, 2)); } else { $r = new \ReflectionFunction($controller); } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 29cb4875d2a5d..ad352160822ae 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -2199,6 +2199,7 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->end() ->arrayNode('envelope') ->info('Mailer Envelope configuration') + ->fixXmlConfig('recipient') ->children() ->scalarNode('sender')->end() ->arrayNode('recipients') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index ffa47c3845fc0..aedd4a86fd113 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -781,7 +781,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index 2311df0fcf36e..f70d60429e90f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -219,6 +219,6 @@ ]) ->set('serializer.normalizer.backed_enum', BackedEnumNormalizer::class) - ->tag('serializer.normalizer', ['priority' => -915]) + ->tag('serializer.normalizer', ['priority' => -880]) ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php index a707c02662fd2..68387298270a3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php @@ -12,7 +12,7 @@ 'dsn' => 'smtp://example.com', 'envelope' => [ 'sender' => 'sender@example.org', - 'recipients' => ['redirected@example.org', 'redirected1@example.org'], + 'recipients' => ['redirected@example.org'], ], 'headers' => [ 'from' => 'from@example.org', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml index 02ecd32e757fc..3436cf417caf7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml @@ -12,8 +12,7 @@ sender@example.org - redirected@example.org - redirected1@example.org + redirected@example.org from@example.org bcc1@example.org diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml index 650e6792dd198..1cd8523b680f4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml @@ -14,8 +14,8 @@ smtp://example2.com sender@example.org - redirected@example.org - redirected1@example.org + redirected@example.org + redirected1@example.org from@example.org bcc1@example.org diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml index e2793a9e7b7f0..e826d6bdcff97 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml @@ -10,7 +10,6 @@ framework: sender: sender@example.org recipients: - redirected@example.org - - redirected1@example.org headers: from: from@example.org bcc: [bcc1@example.org, bcc2@example.org] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index e24031829305f..7e30456900ea7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -361,7 +361,7 @@ public function testWorkflows() $this->assertSame('state_machine.pull_request.metadata_store', (string) $metadataStoreReference); $metadataStoreDefinition = $container->getDefinition('state_machine.pull_request.metadata_store'); - $this->assertSame(Workflow\Metadata\InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); + $this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); $this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); $workflowMetadata = $metadataStoreDefinition->getArgument(0); @@ -2056,21 +2056,27 @@ public function testHttpClientFullDefaultOptions() $this->assertSame(['foo' => ['bar' => 'baz']], $defaultOptions['extra']); } - public static function provideMailer(): array + public static function provideMailer(): iterable { - return [ - ['mailer_with_dsn', ['main' => 'smtp://example.com']], - ['mailer_with_transports', [ + yield [ + 'mailer_with_dsn', + ['main' => 'smtp://example.com'], + ['redirected@example.org'], + ]; + yield [ + 'mailer_with_transports', + [ 'transport1' => 'smtp://example1.com', 'transport2' => 'smtp://example2.com', - ]], + ], + ['redirected@example.org', 'redirected1@example.org'], ]; } /** * @dataProvider provideMailer */ - public function testMailer(string $configFile, array $expectedTransports) + public function testMailer(string $configFile, array $expectedTransports, array $expectedRecipients) { $container = $this->createContainerFromFile($configFile); @@ -2082,7 +2088,7 @@ public function testMailer(string $configFile, array $expectedTransports) $this->assertTrue($container->hasDefinition('mailer.envelope_listener')); $l = $container->getDefinition('mailer.envelope_listener'); $this->assertSame('sender@example.org', $l->getArgument(0)); - $this->assertSame(['redirected@example.org', 'redirected1@example.org'], $l->getArgument(1)); + $this->assertSame($expectedRecipients, $l->getArgument(1)); $this->assertEquals(new Reference('messenger.default_bus', ContainerInterface::NULL_ON_INVALID_REFERENCE), $container->getDefinition('mailer.mailer')->getArgument(1)); $this->assertTrue($container->hasDefinition('mailer.message_listener')); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TranslatableBackedEnum.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TranslatableBackedEnum.php new file mode 100644 index 0000000000000..775276d84a87d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TranslatableBackedEnum.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum TranslatableBackedEnum: string implements TranslatableInterface +{ + case Get = 'GET'; + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + return match ($this) { + self::Get => 'custom_get_string', + }; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php index 630bfb7cd3004..2856816d187a1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\TranslatableBackedEnum; + /** * @author Kévin Dunglas */ @@ -67,6 +69,15 @@ public static function provideNormalizersAndEncodersWithDefaultContextOption(): ['serializer.encoder.csv.alias'], ]; } + + public function testSerializeTranslatableBackedEnum() + { + static::bootKernel(['test_case' => 'Serializer']); + + $serializer = static::getContainer()->get('serializer.alias'); + + $this->assertEquals('GET', $serializer->serialize(TranslatableBackedEnum::Get, 'yaml')); + } } class Foo diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index 667bc4f93da04..2c0562e4066a3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -131,6 +131,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep // collect voters and access decision manager information if ($this->accessDecisionManager instanceof TraceableAccessDecisionManager) { $this->data['voter_strategy'] = $this->accessDecisionManager->getStrategy(); + $this->data['voters'] = []; foreach ($this->accessDecisionManager->getVoters() as $voter) { if ($voter instanceof TraceableVoter) { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index 65bc90cd8487e..bee9a14c8d259 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php @@ -397,6 +397,36 @@ public function dispatch(object $event, ?string $eventName = null): object $this->assertSame($dataCollector->getVoterStrategy(), $strategy, 'Wrong value returned by getVoterStrategy'); } + public function testGetVotersIfAccessDecisionManagerHasNoVoters() + { + $strategy = MainConfiguration::STRATEGY_AFFIRMATIVE; + + $accessDecisionManager = $this->createMock(TraceableAccessDecisionManager::class); + + $accessDecisionManager + ->method('getStrategy') + ->willReturn($strategy); + + $accessDecisionManager + ->method('getVoters') + ->willReturn([]); + + $accessDecisionManager + ->method('getDecisionLog') + ->willReturn([[ + 'attributes' => ['view'], + 'object' => new \stdClass(), + 'result' => true, + 'voterDetails' => [], + ]]); + + $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager, null, null, true); + + $dataCollector->collect(new Request(), new Response()); + + $this->assertEmpty($dataCollector->getVoters()); + } + public static function provideRoles(): array { return [ diff --git a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php index 13d8fd3ba3301..c79b739594c3e 100644 --- a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php @@ -374,10 +374,10 @@ private function getServerVersion(): string private function isTableMissing(\PDOException $exception): bool { $driver = $this->getDriver(); - $code = $exception->getCode(); + [$sqlState, $code] = $exception->errorInfo ?? [null, $exception->getCode()]; return match ($driver) { - 'pgsql' => '42P01' === $code, + 'pgsql' => '42P01' === $sqlState, 'sqlite' => str_contains($exception->getMessage(), 'no such table:'), 'oci' => 942 === $code, 'sqlsrv' => 208 === $code, diff --git a/src/Symfony/Component/Cache/Tests/Fixtures/DriverWrapper.php b/src/Symfony/Component/Cache/Tests/Fixtures/DriverWrapper.php index f0d97724a4e3f..0f7337fe6e913 100644 --- a/src/Symfony/Component/Cache/Tests/Fixtures/DriverWrapper.php +++ b/src/Symfony/Component/Cache/Tests/Fixtures/DriverWrapper.php @@ -32,7 +32,7 @@ public function connect(array $params, $username = null, $password = null, array return $this->driver->connect($params, $username, $password, $driverOptions); } - public function getDatabasePlatform(ServerVersionProvider $versionProvider = null): AbstractPlatform + public function getDatabasePlatform(?ServerVersionProvider $versionProvider = null): AbstractPlatform { return $this->driver->getDatabasePlatform($versionProvider); } diff --git a/src/Symfony/Component/Cache/Tests/Traits/RedisProxiesTest.php b/src/Symfony/Component/Cache/Tests/Traits/RedisProxiesTest.php index 8d315ffd2c519..0e3f2de0b5ec3 100644 --- a/src/Symfony/Component/Cache/Tests/Traits/RedisProxiesTest.php +++ b/src/Symfony/Component/Cache/Tests/Traits/RedisProxiesTest.php @@ -86,32 +86,31 @@ public function testRelayProxy() */ public function testRedis6Proxy($class, $stub) { - $stub = file_get_contents("https://raw.githubusercontent.com/phpredis/phpredis/develop/{$stub}.stub.php"); + if (version_compare(phpversion('redis'), '6.0.2', '>')) { + $stub = file_get_contents("https://raw.githubusercontent.com/phpredis/phpredis/develop/{$stub}.stub.php"); + } else { + $stub = file_get_contents("https://raw.githubusercontent.com/phpredis/phpredis/6.0.2/{$stub}.stub.php"); + } + $stub = preg_replace('/^class /m', 'return; \0', $stub); $stub = preg_replace('/^return; class ([a-zA-Z]++)/m', 'interface \1StubInterface', $stub, 1); $stub = preg_replace('/^ public const .*/m', '', $stub); eval(substr($stub, 5)); - $r = new \ReflectionClass($class.'StubInterface'); - $proxy = file_get_contents(\dirname(__DIR__, 2)."/Traits/{$class}6Proxy.php"); - $proxy = substr($proxy, 0, 4 + strpos($proxy, '[];')); + $this->assertEquals(self::dumpMethods(new \ReflectionClass($class.'StubInterface')), self::dumpMethods(new \ReflectionClass(sprintf('Symfony\Component\Cache\Traits\%s6Proxy', $class)))); + } + + private static function dumpMethods(\ReflectionClass $class): string + { $methods = []; - foreach ($r->getMethods() as $method) { + foreach ($class->getMethods() as $method) { if ('reset' === $method->name || method_exists(LazyProxyTrait::class, $method->name)) { continue; } + $return = $method->getReturnType() instanceof \ReflectionNamedType && 'void' === (string) $method->getReturnType() ? '' : 'return '; $signature = ProxyHelper::exportSignature($method, false, $args); - - if ('Redis' === $class && 'mget' === $method->name) { - $signature = str_replace(': \Redis|array|false', ': \Redis|array', $signature); - } - - if ('RedisCluster' === $class && 'publish' === $method->name) { - $signature = str_replace(': \RedisCluster|bool|int', ': \RedisCluster|bool', $signature); - } - $methods[] = "\n ".str_replace('timeout = 0.0', 'timeout = 0', $signature)."\n".<<lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$method->name}({$args}); @@ -120,9 +119,8 @@ public function testRedis6Proxy($class, $stub) EOPHP; } - uksort($methods, 'strnatcmp'); - $proxy .= implode('', $methods)."}\n"; + usort($methods, 'strnatcmp'); - $this->assertStringEqualsFile(\dirname(__DIR__, 2)."/Traits/{$class}6Proxy.php", $proxy); + return implode("\n", $methods); } } diff --git a/src/Symfony/Component/Cache/Traits/Redis6Proxy.php b/src/Symfony/Component/Cache/Traits/Redis6Proxy.php index 59ab11b0f3c55..e41c0d10cc030 100644 --- a/src/Symfony/Component/Cache/Traits/Redis6Proxy.php +++ b/src/Symfony/Component/Cache/Traits/Redis6Proxy.php @@ -28,6 +28,7 @@ class Redis6Proxy extends \Redis implements ResetInterface, LazyObjectInterface use LazyProxyTrait { resetLazyObject as reset; } + use Redis6ProxyTrait; private const LAZY_OBJECT_PROPERTY_SCOPES = []; @@ -96,11 +97,6 @@ public function bgrewriteaof(): \Redis|bool return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgrewriteaof(...\func_get_args()); } - public function waitaof($numlocal, $numreplicas, $timeout): \Redis|array|false - { - return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args()); - } - public function bitcount($key, $start = 0, $end = -1, $bybit = false): \Redis|false|int { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bitcount(...\func_get_args()); @@ -231,11 +227,6 @@ public function discard(): \Redis|bool return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->discard(...\func_get_args()); } - public function dump($key): \Redis|string - { - return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->dump(...\func_get_args()); - } - public function echo($str): \Redis|false|string { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->echo(...\func_get_args()); @@ -656,11 +647,6 @@ public function ltrim($key, $start, $end): \Redis|bool return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->ltrim(...\func_get_args()); } - public function mget($keys): \Redis|array - { - return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->mget(...\func_get_args()); - } - public function migrate($host, $port, $key, $dstdb, $timeout, $copy = false, $replace = false, #[\SensitiveParameter] $credentials = null): \Redis|bool { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->migrate(...\func_get_args()); diff --git a/src/Symfony/Component/Cache/Traits/Redis6ProxyTrait.php b/src/Symfony/Component/Cache/Traits/Redis6ProxyTrait.php new file mode 100644 index 0000000000000..d086d5b3e8a09 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/Redis6ProxyTrait.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +if (version_compare(phpversion('redis'), '6.0.2', '>')) { + /** + * @internal + */ + trait Redis6ProxyTrait + { + public function dump($key): \Redis|false|string + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->dump(...\func_get_args()); + } + + public function mget($keys): \Redis|array|false + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->mget(...\func_get_args()); + } + + public function waitaof($numlocal, $numreplicas, $timeout): \Redis|array|false + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args()); + } + } +} else { + /** + * @internal + */ + trait Redis6ProxyTrait + { + public function dump($key): \Redis|string + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->dump(...\func_get_args()); + } + + public function mget($keys): \Redis|array + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->mget(...\func_get_args()); + } + } +} diff --git a/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php b/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php index da6526b6eb5d1..6e06b075f27d7 100644 --- a/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php +++ b/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php @@ -28,6 +28,7 @@ class RedisCluster6Proxy extends \RedisCluster implements ResetInterface, LazyOb use LazyProxyTrait { resetLazyObject as reset; } + use RedisCluster6ProxyTrait; private const LAZY_OBJECT_PROPERTY_SCOPES = []; @@ -96,11 +97,6 @@ public function bgrewriteaof($key_or_address): \RedisCluster|bool return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgrewriteaof(...\func_get_args()); } - public function waitaof($key_or_address, $numlocal, $numreplicas, $timeout): \RedisCluster|array|false - { - return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args()); - } - public function bgsave($key_or_address): \RedisCluster|bool { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgsave(...\func_get_args()); @@ -661,11 +657,6 @@ public function pttl($key): \RedisCluster|false|int return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pttl(...\func_get_args()); } - public function publish($channel, $message): \RedisCluster|bool - { - return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->publish(...\func_get_args()); - } - public function pubsub($key_or_address, ...$values): mixed { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pubsub(...\func_get_args()); diff --git a/src/Symfony/Component/Cache/Traits/RedisCluster6ProxyTrait.php b/src/Symfony/Component/Cache/Traits/RedisCluster6ProxyTrait.php new file mode 100644 index 0000000000000..389c6e1adf347 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/RedisCluster6ProxyTrait.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +if (version_compare(phpversion('redis'), '6.0.2', '>')) { + /** + * @internal + */ + trait RedisCluster6ProxyTrait + { + public function publish($channel, $message): \RedisCluster|bool|int + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->publish(...\func_get_args()); + } + + public function waitaof($key_or_address, $numlocal, $numreplicas, $timeout): \RedisCluster|array|false + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args()); + } + } +} else { + /** + * @internal + */ + trait RedisCluster6ProxyTrait + { + public function publish($channel, $message): \RedisCluster|bool + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->publish(...\func_get_args()); + } + } +} diff --git a/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php b/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php index 67caa05a5bc3a..abcf1bd9e9745 100644 --- a/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php +++ b/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php @@ -18,7 +18,6 @@ use Symfony\Component\Config\Definition\NodeInterface; use Symfony\Component\Config\Definition\PrototypedArrayNode; use Symfony\Component\Config\Definition\ScalarNode; -use Symfony\Component\Config\Definition\VariableNode; use Symfony\Component\Yaml\Inline; /** @@ -99,19 +98,12 @@ private function writeNode(NodeInterface $node, ?NodeInterface $parentNode = nul $children = $this->getPrototypeChildren($node); } - if (!$children) { - if ($node->hasDefaultValue() && \count($defaultArray = $node->getDefaultValue())) { - $default = ''; - } elseif (!\is_array($example)) { - $default = '[]'; - } + if (!$children && !($node->hasDefaultValue() && \count($defaultArray = $node->getDefaultValue()))) { + $default = '[]'; } } elseif ($node instanceof EnumNode) { $comments[] = 'One of '.$node->getPermissibleValues('; '); $default = $node->hasDefaultValue() ? Inline::dump($node->getDefaultValue()) : '~'; - } elseif (VariableNode::class === $node::class && \is_array($example)) { - // If there is an array example, we are sure we dont need to print a default value - $default = ''; } else { $default = '~'; @@ -179,7 +171,7 @@ private function writeNode(NodeInterface $node, ?NodeInterface $parentNode = nul $this->writeLine('# '.$message.':', $depth * 4 + 4); - $this->writeArray(array_map(Inline::dump(...), $example), $depth + 1); + $this->writeArray(array_map(Inline::dump(...), $example), $depth + 1, true); } if ($children) { @@ -200,7 +192,7 @@ private function writeLine(string $text, int $indent = 0): void $this->reference .= sprintf($format, $text)."\n"; } - private function writeArray(array $array, int $depth): void + private function writeArray(array $array, int $depth, bool $asComment = false): void { $isIndexed = array_is_list($array); @@ -211,14 +203,16 @@ private function writeArray(array $array, int $depth): void $val = $value; } + $prefix = $asComment ? '# ' : ''; + if ($isIndexed) { - $this->writeLine('- '.$val, $depth * 4); + $this->writeLine($prefix.'- '.$val, $depth * 4); } else { - $this->writeLine(sprintf('%-20s %s', $key.':', $val), $depth * 4); + $this->writeLine(sprintf('%s%-20s %s', $prefix, $key.':', $val), $depth * 4); } if (\is_array($value)) { - $this->writeArray($value, $depth + 1); + $this->writeArray($value, $depth + 1, $asComment); } } } diff --git a/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php b/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php index e6ce07588f9d0..84d9f596c1892 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php @@ -109,6 +109,8 @@ enum="" + + EOL diff --git a/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php b/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php index 18ad445c3ef5d..cb33603f6cbb0 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php @@ -114,11 +114,11 @@ enum: ~ # One of "this"; "that"; Symfony\Component\Config\Tests\ # which should be indented child3: ~ # Example: 'example setting' scalar_prototyped: [] - variable: + variable: ~ # Examples: - - foo - - bar + # - foo + # - bar parameters: # Prototype: Parameter name @@ -142,6 +142,11 @@ enum: ~ # One of "this"; "that"; Symfony\Component\Config\Tests\ # Prototype name: [] + array_with_array_example_and_no_default_value: [] + + # Examples: + # - foo + # - bar custom_node: true EOL; diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Configuration/ExampleConfiguration.php b/src/Symfony/Component/Config/Tests/Fixtures/Configuration/ExampleConfiguration.php index bdf6d80bff443..9f62a684a38fa 100644 --- a/src/Symfony/Component/Config/Tests/Fixtures/Configuration/ExampleConfiguration.php +++ b/src/Symfony/Component/Config/Tests/Fixtures/Configuration/ExampleConfiguration.php @@ -97,6 +97,9 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->arrayNode('array_with_array_example_and_no_default_value') + ->example(['foo', 'bar']) + ->end() ->append(new CustomNodeDefinition('acme')) ->end() ; diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index f4eec051c19a7..b406292b44b3a 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -183,9 +183,9 @@ public function setMessage(string $message, string $name = 'message'): void $this->messages[$name] = $message; } - public function getMessage(string $name = 'message'): string + public function getMessage(string $name = 'message'): ?string { - return $this->messages[$name]; + return $this->messages[$name] ?? null; } public function getStartTime(): int diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index 6aad9e95b8c68..1f026dc504adb 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -371,8 +371,9 @@ public function render() if ($headers && !$containsColspan) { if (0 === $idx) { $rows[] = [sprintf( - '%s: %s', - str_pad($headers[$i] ?? '', $maxHeaderLength, ' ', \STR_PAD_LEFT), + '%s%s: %s', + str_repeat(' ', $maxHeaderLength - Helper::width(Helper::removeDecoration($formatter, $headers[$i] ?? ''))), + $headers[$i] ?? '', $part )]; } else { diff --git a/src/Symfony/Component/Console/Tests/Helper/TableTest.php b/src/Symfony/Component/Console/Tests/Helper/TableTest.php index 728ea847f031f..4af12e34c0680 100644 --- a/src/Symfony/Component/Console/Tests/Helper/TableTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/TableTest.php @@ -1649,6 +1649,28 @@ public static function provideRenderVerticalTests(): \Traversable $books, ]; + yield 'With multibyte characters in some headers (the "í" in "Títle") and cells (the "í" in "Dívíne")' => [ + << [ << $tags) { foreach ($tags as $attributes) { $tag = $this->document->createElement('tag'); - if (!\array_key_exists('name', $attributes)) { - $tag->setAttribute('name', $name); - } else { - $tag->appendChild($this->document->createTextNode($name)); - } // Check if we have recursive attributes if (array_filter($attributes, \is_array(...))) { + $tag->setAttribute('name', $name); $this->addTagRecursiveAttributes($tag, $attributes); } else { + if (!\array_key_exists('name', $attributes)) { + $tag->setAttribute('name', $name); + } else { + $tag->appendChild($this->document->createTextNode($name)); + } foreach ($attributes as $key => $value) { $tag->setAttribute($key, $value ?? ''); } diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php index 520977763f3ad..b8f31ee41e94e 100644 --- a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php @@ -26,7 +26,7 @@ interface DumperInterface * @param bool|null &$asGhostObject Set to true after the call if the proxy is a ghost object * @param string|null $id */ - public function isProxyCandidate(Definition $definition/* , bool &$asGhostObject = null, string $id = null */): bool; + public function isProxyCandidate(Definition $definition/* , ?bool &$asGhostObject = null, ?string $id = null */): bool; /** * Generates the code to be used to instantiate a proxy in the dumped factory code. @@ -38,5 +38,5 @@ public function getProxyFactoryCode(Definition $definition, string $id, string $ * * @param string|null $id */ - public function getProxyCode(Definition $definition/* , string $id = null */): string; + public function getProxyCode(Definition $definition/* , ?string $id = null */): string; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 3588a5b2a594a..de8fea8ab4256 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -35,6 +35,7 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic; +use Symfony\Component\DependencyInjection\Tests\Fixtures\OptionalParameter; use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget; use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTargetAnonymous; use Symfony\Component\DependencyInjection\TypedReference; @@ -405,6 +406,9 @@ public function testResolveParameter() $this->assertEquals(Foo::class, $container->getDefinition('bar')->getArgument(0)); } + /** + * @group legacy + */ public function testOptionalParameter() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php index f99a3f9eb5196..4a7d87358aad8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php @@ -15,12 +15,12 @@ class Bar implements BarInterface { public $quz; - public function __construct($quz = null, \NonExistent $nonExistent = null, BarInterface $decorated = null, array $foo = [], iterable $baz = []) + public function __construct($quz = null, ?\NonExistent $nonExistent = null, ?BarInterface $decorated = null, array $foo = [], iterable $baz = []) { $this->quz = $quz; } - public static function create(\NonExistent $nonExistent = null, $factory = null) + public static function create(?\NonExistent $nonExistent = null, $factory = null) { } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php index 65437a63ec743..53f8bb7c3221e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php @@ -20,7 +20,7 @@ public function setFoosVariadic(Foo $foo, Foo ...$foos) $this->foo = $foo; } - public function setFoosOptional(Foo $foo, Foo $fooOptional = null) + public function setFoosOptional(Foo $foo, ?Foo $fooOptional = null) { $this->foo = $foo; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php index 4f348895132ca..98ee3a45a6036 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php @@ -6,7 +6,7 @@ class BarOptionalArgument { public $foo; - public function __construct(\stdClass $foo = null) + public function __construct(?\stdClass $foo = null) { $this->foo = $foo; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Foo.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Foo.php index e775def689305..36f027f1dd9c6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Foo.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Foo.php @@ -9,7 +9,7 @@ public static function createBar() return new Bar(new \stdClass()); } - public static function createBarArguments(\stdClass $stdClass, \stdClass $stdClassOptional = null) + public static function createBarArguments(\stdClass $stdClass, ?\stdClass $stdClassOptional = null) { return new Bar($stdClass); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/OptionalParameter.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/OptionalParameter.php new file mode 100644 index 0000000000000..8674c648e9005 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/OptionalParameter.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\Tests\Compiler\A; +use Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface; +use Symfony\Component\DependencyInjection\Tests\Compiler\Foo; + +class OptionalParameter +{ + public function __construct(?CollisionInterface $c = null, A $a, ?Foo $f = null) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php index 807c8b3e20086..0659c968c8e72 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php @@ -8,7 +8,7 @@ #[When(env: 'dev')] class Foo implements FooInterface, Sub\BarInterface { - public function __construct($bar = null, iterable $foo = null, object $baz = null) + public function __construct($bar = null, ?iterable $foo = null, ?object $baz = null) { } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_non_scalar_tags.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_non_scalar_tags.php index 76c69868cc49a..2a1234fa7e26a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_non_scalar_tags.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_non_scalar_tags.php @@ -10,6 +10,7 @@ $container ->register('foo', FooClass::class) ->addTag('foo_tag', [ + 'name' => 'attributeName', 'foo' => 'bar', 'bar' => [ 'foo' => 'bar', diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php index a8e71068775b4..a9ac5c0bff430 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php @@ -30,7 +30,7 @@ class Foo public static int $counter = 0; #[Required] - public function cloneFoo(\stdClass $bar = null): static + public function cloneFoo(?\stdClass $bar = null): static { ++self::$counter; @@ -120,7 +120,7 @@ public function __construct(A $a, DInterface $d) class E { - public function __construct(D $d = null) + public function __construct(?D $d = null) { } } @@ -176,13 +176,6 @@ public function __construct(Dunglas $j, Dunglas $k) } } -class OptionalParameter -{ - public function __construct(CollisionInterface $c = null, A $a, Foo $f = null) - { - } -} - class BadTypeHintedArgument { public function __construct(Dunglas $k, NotARealClass $r) @@ -216,7 +209,7 @@ public function __construct(A $k, $foo, Dunglas $dunglas, array $bar) class MultipleArgumentsOptionalScalar { - public function __construct(A $a, $foo = 'default_val', Lille $lille = null) + public function __construct(A $a, $foo = 'default_val', ?Lille $lille = null) { } } @@ -240,7 +233,7 @@ public function __construct( */ class ClassForResource { - public function __construct($foo, Bar $bar = null) + public function __construct($foo, ?Bar $bar = null) { } @@ -455,7 +448,7 @@ public function setBar() { } - public function setOptionalNotAutowireable(NotARealClass $n = null) + public function setOptionalNotAutowireable(?NotARealClass $n = null) { } @@ -513,7 +506,7 @@ class DecoratorImpl implements DecoratorInterface class Decorated implements DecoratorInterface { - public function __construct($quz = null, \NonExistent $nonExistent = null, DecoratorInterface $decorated = null, array $foo = []) + public function __construct($quz = null, ?\NonExistent $nonExistent = null, ?DecoratorInterface $decorated = null, array $foo = []) { } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php index 69ca09218812c..0c7cc2a7b7baf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php @@ -107,7 +107,7 @@ public function __construct(string $arg1, #[AutowireDecorated] AsDecoratorInterf #[AsDecorator(decorates: \NonExistent::class, onInvalid: ContainerInterface::NULL_ON_INVALID_REFERENCE)] class AsDecoratorBaz implements AsDecoratorInterface { - public function __construct(#[AutowireDecorated] AsDecoratorInterface $inner = null) + public function __construct(#[AutowireDecorated] ?AsDecoratorInterface $inner = null) { } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php index 846c8fe64797b..6084c42c77dd4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php @@ -83,7 +83,7 @@ public function callPassed() class DummyProxyDumper implements DumperInterface { - public function isProxyCandidate(Definition $definition, bool &$asGhostObject = null, string $id = null): bool + public function isProxyCandidate(Definition $definition, ?bool &$asGhostObject = null, ?string $id = null): bool { $asGhostObject = false; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_array_tags.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_array_tags.xml index 8e910be31431c..ba8d790571e8b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_array_tags.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_array_tags.xml @@ -4,6 +4,7 @@ + attributeName bar bar diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_array_tags.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_array_tags.yml index 3f580df3e30ef..e4f355c045699 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_array_tags.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_array_tags.yml @@ -7,4 +7,4 @@ services: foo: class: Bar\FooClass tags: - - foo_tag: { foo: bar, bar: { foo: bar, bar: foo } } + - foo_tag: { name: attributeName, foo: bar, bar: { foo: bar, bar: foo } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index c61fb0be569bc..d53fe9398d468 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -468,7 +468,7 @@ public function testParseServiceTagsWithArrayAttributes() $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); $loader->load('services_with_array_tags.xml'); - $this->assertEquals(['foo_tag' => [['foo' => 'bar', 'bar' => ['foo' => 'bar', 'bar' => 'foo']]]], $container->getDefinition('foo')->getTags()); + $this->assertEquals(['foo_tag' => [['name' => 'attributeName', 'foo' => 'bar', 'bar' => ['foo' => 'bar', 'bar' => 'foo']]]], $container->getDefinition('foo')->getTags()); } public function testParseTagsWithoutNameThrowsException() diff --git a/src/Symfony/Component/ErrorHandler/ErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorHandler.php index c096eaca3886a..d82ce6c7024cc 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorHandler.php @@ -595,6 +595,10 @@ public static function handleFatalError(?array $error = null): void set_exception_handler($h); } if (!$handler) { + if (null === $error && $exitCode = self::$exitCode) { + register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); }); + } + return; } if ($handler !== $h) { @@ -630,8 +634,7 @@ public static function handleFatalError(?array $error = null): void // Ignore this re-throw } - if ($exit && self::$exitCode) { - $exitCode = self::$exitCode; + if ($exit && $exitCode = self::$exitCode) { register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); }); } } diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php index f216a8fba63e1..2e13fc40e71b7 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php @@ -31,6 +31,13 @@ */ class ErrorHandlerTest extends TestCase { + protected function tearDown(): void + { + $r = new \ReflectionProperty(ErrorHandler::class, 'exitCode'); + $r->setAccessible(true); + $r->setValue(null, 0); + } + public function testRegister() { $handler = ErrorHandler::register(); diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/FileLinkFormatterTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/FileLinkFormatterTest.php index a5f6330679ff9..fd6d44e316af1 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/FileLinkFormatterTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/FileLinkFormatterTest.php @@ -27,6 +27,11 @@ public function testWhenNoFileLinkFormatAndNoRequest() public function testAfterUnserialize() { + if (get_cfg_var('xdebug.file_link_format')) { + // There is no way to override "xdebug.file_link_format" option in a test. + $this->markTestSkipped('php.ini has a custom option for "xdebug.file_link_format".'); + } + $ide = $_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? null; $_ENV['SYMFONY_IDE'] = $_SERVER['SYMFONY_IDE'] = null; $sut = unserialize(serialize(new FileLinkFormatter())); diff --git a/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ClassWithAnnotatedParameters.php b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ClassWithAnnotatedParameters.php index 2bac262ddb49d..a9cf0dfcb4d2b 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ClassWithAnnotatedParameters.php +++ b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ClassWithAnnotatedParameters.php @@ -14,14 +14,14 @@ public function fooMethod(string $foo) /** * @param string $bar parameter not implemented yet */ - public function barMethod(/* string $bar = null */) + public function barMethod(/* ?string $bar = null */) { } /** * @param Quz $quz parameter not implemented yet */ - public function quzMethod(/* Quz $quz = null */) + public function quzMethod(/* ?Quz $quz = null */) { } diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 0a25f882479d7..48c66a8e41b1d 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -74,6 +74,9 @@ public function copy(string $originFile, string $targetFile, bool $overwriteNewe // Like `cp`, preserve executable permission bits self::box('chmod', $targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111)); + // Like `cp`, preserve the file modification time + self::box('touch', $targetFile, filemtime($originFile)); + if ($bytesCopied !== $bytesOrigin = filesize($originFile)) { throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile); } @@ -198,7 +201,7 @@ private static function doRemove(array $files, bool $isRecursive): void throw new IOException(sprintf('Failed to remove directory "%s": ', $file).$lastError); } - } elseif (!self::box('unlink', $file) && (str_contains(self::$lastError, 'Permission denied') || file_exists($file))) { + } elseif (!self::box('unlink', $file) && ((self::$lastError && str_contains(self::$lastError, 'Permission denied')) || file_exists($file))) { throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError); } } diff --git a/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php b/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php index cb8ed6a775140..bf4c1466c5894 100644 --- a/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php +++ b/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php @@ -28,7 +28,7 @@ class MockStream * @param string|null $opened_path If the path is opened successfully, and STREAM_USE_PATH is set in options, * opened_path should be set to the full path of the file/resource that was actually opened */ - public function stream_open(string $path, string $mode, int $options, string &$opened_path = null): bool + public function stream_open(string $path, string $mode, int $options, ?string &$opened_path = null): bool { return true; } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php index 77b1e75bd49a5..96bdc7c0de1a1 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php @@ -109,6 +109,10 @@ public function reverseTransform(mixed $value): ?\DateTime throw new TransformationFailedException('Expected a string.'); } + if (str_contains($value, "\0")) { + throw new TransformationFailedException('Null bytes not allowed'); + } + $outputTz = new \DateTimeZone($this->outputTimezone); $dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php index 66ad9ff416e26..f7ef667e769b6 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php @@ -133,6 +133,19 @@ public function testReverseTransformEmpty() $this->assertNull($reverseTransformer->reverseTransform('')); } + public function testReverseTransformWithNullBytes() + { + $transformer = new DateTimeToStringTransformer(); + + $nullByte = \chr(0); + $value = '2024-03-15 21:11:00'.$nullByte; + + $this->expectException(TransformationFailedException::class); + $this->expectExceptionMessage('Null bytes not allowed'); + + $transformer->reverseTransform($value); + } + public function testReverseTransformWithDifferentTimezones() { $reverseTransformer = new DateTimeToStringTransformer('America/New_York', 'Asia/Hong_Kong', 'Y-m-d H:i:s'); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTestCase.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTestCase.php index e86bf9e41ed13..5238e2fd88098 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTestCase.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTestCase.php @@ -40,16 +40,6 @@ public function testPassIdAndNameToView() $this->assertEquals('name', $view->vars['full_name']); } - public function testStripLeadingUnderscoresAndDigitsFromId() - { - $view = $this->factory->createNamed('_09name', $this->getTestedType(), null, $this->getTestOptions()) - ->createView(); - - $this->assertEquals('name', $view->vars['id']); - $this->assertEquals('_09name', $view->vars['name']); - $this->assertEquals('_09name', $view->vars['full_name']); - } - public function testPassIdAndNameToViewWithParent() { $view = $this->factory->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE) diff --git a/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php b/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php index b37bfe5ed2d85..8be0323ae1a9e 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php @@ -19,7 +19,7 @@ class CustomArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable { private array $array; - public function __construct(array $array = null) + public function __construct(?array $array = null) { $this->array = $array ?: []; } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/FixedTranslator.php b/src/Symfony/Component/Form/Tests/Fixtures/FixedTranslator.php index 1fc0fa90165f8..432f2ab12db90 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/FixedTranslator.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/FixedTranslator.php @@ -22,7 +22,7 @@ public function __construct(array $translations) $this->translations = $translations; } - public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string + public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string { return $this->translations[$id] ?? $id; } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/TranslatableTextAlign.php b/src/Symfony/Component/Form/Tests/Fixtures/TranslatableTextAlign.php index 7a5d5cdff68e7..4464088c78103 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/TranslatableTextAlign.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/TranslatableTextAlign.php @@ -20,7 +20,7 @@ enum TranslatableTextAlign implements TranslatableInterface case Center; case Right; - public function trans(TranslatorInterface $translator, string $locale = null): string + public function trans(TranslatorInterface $translator, ?string $locale = null): string { return $translator->trans($this->name, locale: $locale); } diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index e74e0263b985f..4446a031e9695 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -51,6 +51,9 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, private ?LoggerInterface $logger = null; + private int $maxHostConnections; + private int $maxPendingPushes; + /** * An internal object to share state between the client and its responses. */ @@ -69,18 +72,22 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.'); } + $this->maxHostConnections = $maxHostConnections; + $this->maxPendingPushes = $maxPendingPushes; + $this->defaultOptions['buffer'] ??= self::shouldBuffer(...); if ($defaultOptions) { [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions); } - - $this->multi = new CurlClientState($maxHostConnections, $maxPendingPushes); } public function setLogger(LoggerInterface $logger): void { - $this->logger = $this->multi->logger = $logger; + $this->logger = $logger; + if (isset($this->multi)) { + $this->multi->logger = $logger; + } } /** @@ -88,6 +95,8 @@ public function setLogger(LoggerInterface $logger): void */ public function request(string $method, string $url, array $options = []): ResponseInterface { + $multi = $this->ensureState(); + [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions); $scheme = $url['scheme']; $authority = $url['authority']; @@ -165,24 +174,24 @@ public function request(string $method, string $url, array $options = []): Respo } // curl's resolve feature varies by host:port but ours varies by host only, let's handle this with our own DNS map - if (isset($this->multi->dnsCache->hostnames[$host])) { - $options['resolve'] += [$host => $this->multi->dnsCache->hostnames[$host]]; + if (isset($multi->dnsCache->hostnames[$host])) { + $options['resolve'] += [$host => $multi->dnsCache->hostnames[$host]]; } - if ($options['resolve'] || $this->multi->dnsCache->evictions) { + if ($options['resolve'] || $multi->dnsCache->evictions) { // First reset any old DNS cache entries then add the new ones - $resolve = $this->multi->dnsCache->evictions; - $this->multi->dnsCache->evictions = []; + $resolve = $multi->dnsCache->evictions; + $multi->dnsCache->evictions = []; if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) { // DNS cache removals require curl 7.42 or higher - $this->multi->reset(); + $multi->reset(); } foreach ($options['resolve'] as $host => $ip) { $resolve[] = null === $ip ? "-$host:$port" : "$host:$port:$ip"; - $this->multi->dnsCache->hostnames[$host] = $ip; - $this->multi->dnsCache->removals["-$host:$port"] = "-$host:$port"; + $multi->dnsCache->hostnames[$host] = $ip; + $multi->dnsCache->removals["-$host:$port"] = "-$host:$port"; } $curlopts[\CURLOPT_RESOLVE] = $resolve; @@ -285,8 +294,8 @@ public function request(string $method, string $url, array $options = []): Respo $curlopts += $options['extra']['curl']; } - if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) { - unset($this->multi->pushedResponses[$url]); + if ($pushedResponse = $multi->pushedResponses[$url] ?? null) { + unset($multi->pushedResponses[$url]); if (self::acceptPushForRequest($method, $options, $pushedResponse)) { $this->logger?->debug(sprintf('Accepting pushed response: "%s %s"', $method, $url)); @@ -294,7 +303,7 @@ public function request(string $method, string $url, array $options = []): Respo // Reinitialize the pushed response with request's options $ch = $pushedResponse->handle; $pushedResponse = $pushedResponse->response; - $pushedResponse->__construct($this->multi, $url, $options, $this->logger); + $pushedResponse->__construct($multi, $url, $options, $this->logger); } else { $this->logger?->debug(sprintf('Rejecting pushed response: "%s"', $url)); $pushedResponse = null; @@ -304,7 +313,7 @@ public function request(string $method, string $url, array $options = []): Respo if (!$pushedResponse) { $ch = curl_init(); $this->logger?->info(sprintf('Request: "%s %s"', $method, $url)); - $curlopts += [\CURLOPT_SHARE => $this->multi->share]; + $curlopts += [\CURLOPT_SHARE => $multi->share]; } foreach ($curlopts as $opt => $value) { @@ -314,7 +323,7 @@ public function request(string $method, string $url, array $options = []): Respo } } - return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host, $port), CurlClientState::$curlVersion['version_number'], $url); + return $pushedResponse ?? new CurlResponse($multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host, $port), CurlClientState::$curlVersion['version_number'], $url); } public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface @@ -323,9 +332,11 @@ public function stream(ResponseInterface|iterable $responses, ?float $timeout = $responses = [$responses]; } - if ($this->multi->handle instanceof \CurlMultiHandle) { + $multi = $this->ensureState(); + + if ($multi->handle instanceof \CurlMultiHandle) { $active = 0; - while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active)) { + while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($multi->handle, $active)) { } } @@ -334,7 +345,9 @@ public function stream(ResponseInterface|iterable $responses, ?float $timeout = public function reset(): void { - $this->multi->reset(); + if (isset($this->multi)) { + $this->multi->reset(); + } } /** @@ -434,6 +447,16 @@ private static function createRedirectResolver(array $options, string $host, int }; } + private function ensureState(): CurlClientState + { + if (!isset($this->multi)) { + $this->multi = new CurlClientState($this->maxHostConnections, $this->maxPendingPushes); + $this->multi->logger = $this->logger; + } + + return $this->multi; + } + private function findConstantName(int $opt): ?string { $constants = array_filter(get_defined_constants(), static fn ($v, $k) => $v === $opt && 'C' === $k[0] && (str_starts_with($k, 'CURLOPT_') || str_starts_with($k, 'CURLINFO_')), \ARRAY_FILTER_USE_BOTH); diff --git a/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php b/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php index 66372aa8a8149..9372dbe5a0b0d 100644 --- a/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php @@ -21,7 +21,7 @@ class JsonMockResponse extends MockResponse public function __construct(mixed $body = [], array $info = []) { try { - $json = json_encode($body, \JSON_THROW_ON_ERROR); + $json = json_encode($body, \JSON_THROW_ON_ERROR | \JSON_PRESERVE_ZERO_FRACTION); } catch (\JsonException $e) { throw new InvalidArgumentException('JSON encoding failed: '.$e->getMessage(), $e->getCode(), $e); } diff --git a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php index 266b539ef6b59..20cf7ef48291b 100644 --- a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php @@ -58,8 +58,8 @@ public function testHandleIsReinitOnReset() { $httpClient = $this->getHttpClient(__FUNCTION__); - $r = new \ReflectionProperty($httpClient, 'multi'); - $clientState = $r->getValue($httpClient); + $r = new \ReflectionMethod($httpClient, 'ensureState'); + $clientState = $r->invoke($httpClient); $initialShareId = $clientState->share; $httpClient->reset(); self::assertNotSame($initialShareId, $clientState->share); diff --git a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php index a7493100c431d..91ec7ea5f2c7c 100644 --- a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php +++ b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php @@ -24,6 +24,11 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(); + } + public function testItCollectsRequestCount() { $httpClient1 = $this->httpClientThatHasTracedRequests([ diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php index 613d80cb1d3a7..3e81f429622a0 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php @@ -72,10 +72,8 @@ public function testPrepareRequestWithBodyIsArray() public function testNormalizeBodyMultipart() { $file = fopen('php://memory', 'r+'); - stream_context_set_option($file, ['http' => [ - 'filename' => 'test.txt', - 'content_type' => 'text/plain', - ]]); + stream_context_set_option($file, 'http', 'filename', 'test.txt'); + stream_context_set_option($file, 'http', 'content_type', 'text/plain'); fwrite($file, 'foobarbaz'); rewind($file); diff --git a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php index 247588cd359a3..182d52932f724 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php @@ -32,6 +32,11 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(); + } + public function testSendRequest() { $client = new HttplugClient(new NativeHttpClient()); diff --git a/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php b/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php index d4bae3ab5c4a3..d1f4deb03a006 100644 --- a/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php @@ -28,6 +28,11 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(); + } + public function testSendRequest() { $factory = new Psr17Factory(); diff --git a/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php b/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php index b371c08cf4241..768353b04abd1 100644 --- a/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php +++ b/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php @@ -59,6 +59,22 @@ public function testJsonEncodeString() $this->assertSame('application/json', $response->getHeaders()['content-type'][0]); } + public function testJsonEncodeFloat() + { + $client = new MockHttpClient(new JsonMockResponse([ + 'foo' => 1.23, + 'ccc' => 1.0, + 'baz' => 10., + ])); + $response = $client->request('GET', 'https://symfony.com'); + + $this->assertSame([ + 'foo' => 1.23, + 'ccc' => 1., + 'baz' => 10., + ], $response->toArray()); + } + /** * @dataProvider responseHeadersProvider */ diff --git a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php index ba9504ae1c66d..a0e39cc46c851 100644 --- a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php @@ -27,6 +27,11 @@ class RetryableHttpClientTest extends TestCase { + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(); + } + public function testRetryOnError() { $client = new RetryableHttpClient( diff --git a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php index cf437a653bd76..b6a2c03c8f7a3 100644 --- a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php @@ -29,6 +29,11 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(); + } + public function testItTracesRequest() { $httpClient = $this->createMock(HttpClientInterface::class); diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json index 33fa3b4558004..ef456a603763c 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -25,7 +25,7 @@ "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3", + "symfony/http-client-contracts": "^3.4.1", "symfony/service-contracts": "^2.5|^3" }, "require-dev": { @@ -33,7 +33,7 @@ "amphp/http-client": "^4.2.1", "amphp/http-tunnel": "^1.0", "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4", + "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", diff --git a/src/Symfony/Component/HttpKernel/Event/ControllerEvent.php b/src/Symfony/Component/HttpKernel/Event/ControllerEvent.php index 9e5a3a56e1c30..626163d148b8a 100644 --- a/src/Symfony/Component/HttpKernel/Event/ControllerEvent.php +++ b/src/Symfony/Component/HttpKernel/Event/ControllerEvent.php @@ -70,7 +70,7 @@ public function setController(callable $controller, ?array $attributes = null): if (\is_array($controller) && method_exists(...$controller)) { $this->controllerReflector = new \ReflectionMethod(...$controller); } elseif (\is_string($controller) && str_contains($controller, '::')) { - $this->controllerReflector = new \ReflectionMethod($controller); + $this->controllerReflector = new \ReflectionMethod(...explode('::', $controller, 2)); } else { $this->controllerReflector = new \ReflectionFunction($controller(...)); } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index d3ed631064403..1b57d2b84809c 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.4.5'; - public const VERSION_ID = 60405; + public const VERSION = '6.4.6'; + public const VERSION_ID = 60406; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 5; + public const RELEASE_VERSION = 6; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2026'; diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/DataCollector/CloneVarDataCollector.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/DataCollector/CloneVarDataCollector.php index e1508bf569b4b..ad011fc49de45 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fixtures/DataCollector/CloneVarDataCollector.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/DataCollector/CloneVarDataCollector.php @@ -25,7 +25,7 @@ public function __construct($varToClone) $this->varToClone = $varToClone; } - public function collect(Request $request, Response $response, \Throwable $exception = null): void + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { $this->data = $this->cloneVar($this->varToClone); } diff --git a/src/Symfony/Component/Lock/Store/PdoStore.php b/src/Symfony/Component/Lock/Store/PdoStore.php index da3d968d2d930..5a57faa67bfed 100644 --- a/src/Symfony/Component/Lock/Store/PdoStore.php +++ b/src/Symfony/Component/Lock/Store/PdoStore.php @@ -241,10 +241,10 @@ private function getCurrentTimestampStatement(): string private function isTableMissing(\PDOException $exception): bool { $driver = $this->getDriver(); - $code = $exception->getCode(); + [$sqlState, $code] = $exception->errorInfo ?? [null, $exception->getCode()]; return match ($driver) { - 'pgsql' => '42P01' === $code, + 'pgsql' => '42P01' === $sqlState, 'sqlite' => str_contains($exception->getMessage(), 'no such table:'), 'oci' => 942 === $code, 'sqlsrv' => 208 === $code, diff --git a/src/Symfony/Component/Lock/Store/RedisStore.php b/src/Symfony/Component/Lock/Store/RedisStore.php index 51d3f72a9fd26..138b9b0dcc22e 100644 --- a/src/Symfony/Component/Lock/Store/RedisStore.php +++ b/src/Symfony/Component/Lock/Store/RedisStore.php @@ -294,7 +294,9 @@ private function getNowCode(): string try { $this->supportTime = 1 === $this->evaluate($script, 'symfony_check_support_time', []); } catch (LockStorageException $e) { - if (!str_contains($e->getMessage(), 'commands not allowed after non deterministic')) { + if (!str_contains($e->getMessage(), 'commands not allowed after non deterministic') + && !str_contains($e->getMessage(), 'is not allowed from script script') + ) { throw $e; } $this->supportTime = false; diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/RemoteEvent/BrevoPayloadConverter.php b/src/Symfony/Component/Mailer/Bridge/Brevo/RemoteEvent/BrevoPayloadConverter.php index 8d556b8f37c9a..5f5f0816c23c6 100644 --- a/src/Symfony/Component/Mailer/Bridge/Brevo/RemoteEvent/BrevoPayloadConverter.php +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/RemoteEvent/BrevoPayloadConverter.php @@ -60,7 +60,10 @@ public function convert(array $payload): AbstractMailerEvent $event->setDate($date); $event->setRecipientEmail($payload['email']); - $event->setTags($payload['tags']); + + if (isset($payload['tags'])) { + $event->setTags($payload['tags']); + } return $event; } diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.json b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.json new file mode 100644 index 0000000000000..4fbeadc58112b --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.json @@ -0,0 +1,13 @@ +{ + "id": 814119, + "email": "example@gmail.com", + "message-id": "<202305311313.92192897094@smtp-relay.mailin.fr>", + "date": "2023-05-31 15:13:08", + "event": "request", + "subject": "Subject Line", + "sending_ip": "127.0.0.1", + "ts_event": 1685538788, + "ts": 1685538788, + "reason": "sent", + "ts_epoch": 1685538788179 +} diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.php b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.php new file mode 100644 index 0000000000000..57bb1ff1893ee --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.php @@ -0,0 +1,9 @@ +', json_decode(file_get_contents(str_replace('.php', '.json', __FILE__)), true, flags: JSON_THROW_ON_ERROR)); +$wh->setRecipientEmail('example@gmail.com'); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1685538788)); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-failing-sendmail.php b/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-failing-sendmail.php new file mode 100755 index 0000000000000..920b980e0f714 --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-failing-sendmail.php @@ -0,0 +1,4 @@ +#!/usr/bin/env php +assertStringEqualsFile($this->argsPath, __DIR__.'/Fixtures/fake-sendmail.php -ffrom@mail.com recipient@mail.com'); } + + public function testThrowsTransportExceptionOnFailure() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support shebangs nor non-blocking standard streams'); + } + + $mail = new Email(); + $mail + ->from('from@mail.com') + ->to('to@mail.com') + ->subject('Subject') + ->text('Some text') + ; + + $envelope = new DelayedEnvelope($mail); + $envelope->setRecipients([new Address('recipient@mail.com')]); + + $sendmailTransport = new SendmailTransport(self::FAKE_FAILING_SENDMAIL); + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Process failed with exit code 42: Sending failed'); + $sendmailTransport->send($mail, $envelope); + } } diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/DummyStream.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/DummyStream.php index 407c90810b78b..d67671ea10762 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/DummyStream.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/DummyStream.php @@ -77,7 +77,7 @@ public function write(string $bytes, $debug = true): void } elseif (str_starts_with($bytes, 'QUIT')) { $this->nextResponse = '221 Goodbye'; } else { - $this->nextResponse = '250 OK'; + $this->nextResponse = '250 OK queued as 000501c4054c'; } } diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php index 6900320506053..a8d3540a115f9 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php @@ -13,6 +13,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Event\SentMessageEvent; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; @@ -24,6 +26,7 @@ use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\Part\File; use Symfony\Component\Mime\RawMessage; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @group time-sensitive @@ -137,6 +140,37 @@ public function testWriteEncodedRecipientAndSenderAddresses() $this->assertContains("RCPT TO:\r\n", $stream->getCommands()); } + public function testMessageIdFromServerIsEmbeddedInSentMessageEvent() + { + $calls = 0; + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher->expects($this->any()) + ->method('dispatch') + ->with($this->callback(static function ($event) use (&$calls): bool { + ++$calls; + + if (1 === $calls && $event instanceof MessageEvent) { + return true; + } + + if (2 === $calls && $event instanceof SentMessageEvent && '000501c4054c' === $event->getMessage()->getMessageId()) { + return true; + } + + return false; + })); + $transport = new SmtpTransport(new DummyStream(), $eventDispatcher); + + $email = new Email(); + $email->from('sender@example.com'); + $email->to('recipient@example.com'); + $email->text('.'); + + $transport->send($email); + + $this->assertSame(2, $calls); + } + public function testAssertResponseCodeNoCodes() { $this->expectException(LogicException::class); diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php index e05e347919d27..0de38fb2ed690 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php @@ -39,7 +39,6 @@ class SmtpTransport extends AbstractTransport private int $pingThreshold = 100; private float $lastMessageTime = 0; private AbstractStream $stream; - private string $mtaResult = ''; private string $domain = '[127.0.0.1]'; public function __construct(?AbstractStream $stream = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) @@ -148,10 +147,6 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess throw $e; } - if ($this->mtaResult && $messageId = $this->parseMessageId($this->mtaResult)) { - $message->setMessageId($messageId); - } - $this->checkRestartThreshold(); return $message; @@ -235,9 +230,13 @@ protected function doSend(SentMessage $message): void $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); throw $e; } - $this->mtaResult = $this->executeCommand("\r\n.\r\n", [250]); + $mtaResult = $this->executeCommand("\r\n.\r\n", [250]); $message->appendDebug($this->stream->getDebug()); $this->lastMessageTime = microtime(true); + + if ($mtaResult && $messageId = $this->parseMessageId($mtaResult)) { + $message->setMessageId($messageId); + } } catch (TransportExceptionInterface $e) { $e->appendDebug($this->stream->getDebug()); $this->lastMessageTime = 0; diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/AbstractStream.php b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/AbstractStream.php index a68748b6c9585..498dc560c3ede 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/AbstractStream.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/AbstractStream.php @@ -30,6 +30,7 @@ abstract class AbstractStream protected $in; /** @var resource|null */ protected $out; + protected $err; private string $debug = ''; @@ -68,7 +69,7 @@ abstract public function initialize(): void; public function terminate(): void { - $this->stream = $this->out = $this->in = null; + $this->stream = $this->err = $this->out = $this->in = null; } public function readLine(): string diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php index 3d59ecfecb7d9..8644d7dad7296 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php @@ -45,14 +45,20 @@ public function initialize(): void } $this->in = &$pipes[0]; $this->out = &$pipes[1]; + $this->err = &$pipes[2]; } public function terminate(): void { if (null !== $this->stream) { fclose($this->in); + $out = stream_get_contents($this->out); fclose($this->out); - proc_close($this->stream); + $err = stream_get_contents($this->err); + fclose($this->err); + if (0 !== $exitCode = proc_close($this->stream)) { + throw new TransportException('Process failed with exit code '.$exitCode.': '.$out.$err); + } } parent::terminate(); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php index a08c102a4b9da..e57dad6cad97e 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php @@ -774,6 +774,73 @@ public function testItCanBeConstructedWithTLSOptionsAndNonTLSDsn() ); } + public function testItCanRetryPublishWhenAMQPConnectionExceptionIsThrown() + { + $factory = new TestAmqpFactory( + $amqpConnection = $this->createMock(\AMQPConnection::class), + $amqpChannel = $this->createMock(\AMQPChannel::class), + $amqpQueue = $this->createMock(\AMQPQueue::class), + $amqpExchange = $this->createMock(\AMQPExchange::class) + ); + + $amqpExchange->expects($this->exactly(2)) + ->method('publish') + ->willReturnOnConsecutiveCalls( + $this->throwException(new \AMQPConnectionException('a socket error occurred')), + null + ); + + $connection = Connection::fromDsn('amqp://localhost', [], $factory); + $connection->publish('body'); + } + + public function testItCanRetryPublishWithDelayWhenAMQPConnectionExceptionIsThrown() + { + $factory = new TestAmqpFactory( + $amqpConnection = $this->createMock(\AMQPConnection::class), + $amqpChannel = $this->createMock(\AMQPChannel::class), + $amqpQueue = $this->createMock(\AMQPQueue::class), + $amqpExchange = $this->createMock(\AMQPExchange::class) + ); + + $amqpExchange->expects($this->exactly(2)) + ->method('publish') + ->willReturnOnConsecutiveCalls( + $this->throwException(new \AMQPConnectionException('a socket error occurred')), + null + ); + + $connection = Connection::fromDsn('amqp://localhost', [], $factory); + $connection->publish('body', [], 5000); + } + + public function testItWillRetryMaxThreeTimesWhenAMQPConnectionExceptionIsThrown() + { + $factory = new TestAmqpFactory( + $amqpConnection = $this->createMock(\AMQPConnection::class), + $amqpChannel = $this->createMock(\AMQPChannel::class), + $amqpQueue = $this->createMock(\AMQPQueue::class), + $amqpExchange = $this->createMock(\AMQPExchange::class) + ); + + $exception = new \AMQPConnectionException('a socket error occurred'); + + $amqpExchange->expects($this->exactly(4)) + ->method('publish') + ->willReturnOnConsecutiveCalls( + $this->throwException($exception), + $this->throwException($exception), + $this->throwException($exception), + $this->throwException($exception), + ); + + self::expectException($exception::class); + self::expectExceptionMessage($exception->getMessage()); + + $connection = Connection::fromDsn('amqp://localhost', [], $factory); + $connection->publish('body'); + } + private function createDelayOrRetryConnection(\AMQPExchange $delayExchange, string $deadLetterExchangeName, string $delayQueueName): Connection { $amqpConnection = $this->createMock(\AMQPConnection::class); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php index b39902623adce..0ae1bff21d7c8 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php @@ -287,19 +287,21 @@ public function publish(string $body, array $headers = [], int $delayInMs = 0, ? $this->setupExchangeAndQueues(); // also setup normal exchange for delayed messages so delay queue can DLX messages to it } - if (0 !== $delayInMs) { - $this->publishWithDelay($body, $headers, $delayInMs, $amqpStamp); + $this->withConnectionExceptionRetry(function () use ($body, $headers, $delayInMs, $amqpStamp) { + if (0 !== $delayInMs) { + $this->publishWithDelay($body, $headers, $delayInMs, $amqpStamp); - return; - } + return; + } - $this->publishOnExchange( - $this->exchange(), - $body, - $this->getRoutingKeyForMessage($amqpStamp), - $headers, - $amqpStamp - ); + $this->publishOnExchange( + $this->exchange(), + $body, + $this->getRoutingKeyForMessage($amqpStamp), + $headers, + $amqpStamp + ); + }); } /** @@ -545,11 +547,16 @@ public function exchange(): \AMQPExchange private function clearWhenDisconnected(): void { if (!$this->channel()->isConnected()) { - unset($this->amqpChannel, $this->amqpExchange, $this->amqpDelayExchange); - $this->amqpQueues = []; + $this->clear(); } } + private function clear(): void + { + unset($this->amqpChannel, $this->amqpExchange, $this->amqpDelayExchange); + $this->amqpQueues = []; + } + private function getDefaultPublishRoutingKey(): ?string { return $this->exchangeOptions['default_publish_routing_key'] ?? null; @@ -566,4 +573,23 @@ private function getRoutingKeyForMessage(?AmqpStamp $amqpStamp): ?string { return $amqpStamp?->getRoutingKey() ?? $this->getDefaultPublishRoutingKey(); } + + private function withConnectionExceptionRetry(callable $callable): void + { + $maxRetries = 3; + $retries = 0; + + retry: + try { + $callable(); + } catch (\AMQPConnectionException $e) { + if (++$retries <= $maxRetries) { + $this->clear(); + + goto retry; + } + + throw $e; + } + } } diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php index e3030d3a1d55f..5118e4ff3aaa5 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php @@ -162,6 +162,10 @@ public function get(): ?array $this->driverConnection->delete($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59']); } catch (DriverException $e) { // Ignore the exception + } catch (TableNotFoundException $e) { + if ($this->autoSetup) { + $this->setup(); + } } } diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index 7e68e9f5c2767..43affdabf7223 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -8,7 +8,6 @@ CHANGELOG * Add `HandlerDescriptor::getOptions` * Add support for multiple Redis Sentinel hosts * Add `--all` option to the `messenger:failed:remove` command - * `RejectRedeliveredMessageException` implements `UnrecoverableExceptionInterface` in order to not be retried * Add `WrappedExceptionsInterface` interface for exceptions that hold multiple individual exceptions * Deprecate `HandlerFailedException::getNestedExceptions()`, `HandlerFailedException::getNestedExceptionsOfClass()` and `DelayedMessageHandlingException::getExceptions()` which are replaced by a new `getWrappedExceptions()` method diff --git a/src/Symfony/Component/Messenger/Exception/RejectRedeliveredMessageException.php b/src/Symfony/Component/Messenger/Exception/RejectRedeliveredMessageException.php index 72283878c1b0c..0befccf4a1d1f 100644 --- a/src/Symfony/Component/Messenger/Exception/RejectRedeliveredMessageException.php +++ b/src/Symfony/Component/Messenger/Exception/RejectRedeliveredMessageException.php @@ -14,6 +14,6 @@ /** * @author Tobias Schultze */ -class RejectRedeliveredMessageException extends RuntimeException implements UnrecoverableExceptionInterface +class RejectRedeliveredMessageException extends RuntimeException { } diff --git a/src/Symfony/Component/Messenger/Handler/BatchHandlerInterface.php b/src/Symfony/Component/Messenger/Handler/BatchHandlerInterface.php index a2fce4e1bb1e2..42a8590ee70a8 100644 --- a/src/Symfony/Component/Messenger/Handler/BatchHandlerInterface.php +++ b/src/Symfony/Component/Messenger/Handler/BatchHandlerInterface.php @@ -23,7 +23,7 @@ interface BatchHandlerInterface * @return mixed The number of pending messages in the batch if $ack is not null, * the result from handling the message otherwise */ - // public function __invoke(object $message, Acknowledger $ack = null): mixed; + // public function __invoke(object $message, ?Acknowledger $ack = null): mixed; /** * Flushes any pending buffers. diff --git a/src/Symfony/Component/Mime/Tests/Fixtures/web/index.php b/src/Symfony/Component/Mime/Tests/Fixtures/web/index.php new file mode 100644 index 0000000000000..b3d9bbc7f3711 --- /dev/null +++ b/src/Symfony/Component/Mime/Tests/Fixtures/web/index.php @@ -0,0 +1 @@ +markTestSkipped('"https" stream wrapper is not enabled.'); } - $p = DataPart::fromPath($file = 'https://symfony.com/images/common/logo/logo_symfony_header.png'); + $finder = new PhpExecutableFinder(); + $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', '127.0.0.1:8057'])); + $process->setWorkingDirectory(__DIR__.'/../Fixtures/web'); + $process->start(); + + do { + usleep(50000); + } while (!@fopen('http://127.0.0.1:8057', 'r')); + + $p = DataPart::fromPath($file = 'http://localhost:8057/logo_symfony_header.png'); $content = file_get_contents($file); $this->assertEquals($content, $p->getBody()); $maxLineLength = 76; diff --git a/src/Symfony/Component/Mime/composer.json b/src/Symfony/Component/Mime/composer.json index a769b4efc79d1..c9cf9eaaa0f60 100644 --- a/src/Symfony/Component/Mime/composer.json +++ b/src/Symfony/Component/Mime/composer.json @@ -26,6 +26,7 @@ "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", "symfony/property-access": "^5.4|^6.0|^7.0", "symfony/property-info": "^5.4|^6.0|^7.0", "symfony/serializer": "^6.3.2|^7.0" diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md index 33792585479a4..15840fbd578d9 100644 --- a/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md @@ -21,9 +21,9 @@ CHANGELOG * The bridge is not marked as `@experimental` anymore * [BC BREAK] Change signature of `EsendexTransport::__construct()` method from: - `public function __construct(string $token, string $accountReference, string $from, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)` + `public function __construct(string $token, string $accountReference, string $from, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)` to: - `public function __construct(string $email, string $password, string $accountReference, string $from, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)` + `public function __construct(string $email, string $password, string $accountReference, string $from, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)` 5.2.0 ----- diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md index c01ece62d544a..7f8d65492b39f 100644 --- a/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md @@ -12,9 +12,9 @@ CHANGELOG * The bridge is not marked as `@experimental` anymore * [BC BREAK] Remove `GoogleChatTransport::setThreadKey()` method, this parameter should now be provided via the constructor, which has changed from: - `__construct(string $space, string $accessKey, string $accessToken, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)` + `__construct(string $space, string $accessKey, string $accessToken, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)` to: - `__construct(string $space, string $accessKey, string $accessToken, string $threadKey = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)` + `__construct(string $space, string $accessKey, string $accessToken, ?string $threadKey = null, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)` * [BC BREAK] Rename the parameter `threadKey` to `thread_key` in DSN 5.2.0 diff --git a/src/Symfony/Component/Notifier/Bridge/Mattermost/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Mattermost/CHANGELOG.md index 8e154d13f0b85..39bc172f8cb7b 100644 --- a/src/Symfony/Component/Notifier/Bridge/Mattermost/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/Mattermost/CHANGELOG.md @@ -6,9 +6,9 @@ CHANGELOG * The bridge is not marked as `@experimental` anymore * [BC BREAK] Change signature of `MattermostTransport::__construct()` method from: - `public function __construct(string $token, string $channel, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, string $path = null)` + `public function __construct(string $token, string $channel, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null, ?string $path = null)` to: - `public function __construct(string $token, string $channel, ?string $path = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)` + `public function __construct(string $token, string $channel, ?string $path = null, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)` 5.1.0 ----- diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 60f2d32770b0d..4343c9fc313fe 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -411,8 +411,18 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid throw $e; } } elseif (PropertyReadInfo::TYPE_PROPERTY === $type) { - if ($access->canBeReference() && !isset($object->$name) && !\array_key_exists($name, (array) $object) && !(new \ReflectionProperty($class, $name))->hasType()) { - throw new UninitializedPropertyException(sprintf('The property "%s::$%s" is not initialized.', $class, $name)); + if (!isset($object->$name) && !\array_key_exists($name, (array) $object)) { + try { + $r = new \ReflectionProperty($class, $name); + + if ($r->isPublic() && !$r->hasType()) { + throw new UninitializedPropertyException(sprintf('The property "%s::$%s" is not initialized.', $class, $name)); + } + } catch (\ReflectionException $e) { + if (!$ignoreInvalidProperty) { + throw new NoSuchPropertyException(sprintf('Can\'t get a way to read the property "%s" in class "%s".', $property, $class)); + } + } } $result[self::VALUE] = $object->$name; diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php index ca4074ca3229b..ed9944d27a67a 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php @@ -19,7 +19,7 @@ class NonTraversableArrayObject implements \ArrayAccess, \Countable { private $array; - public function __construct(array $array = null) + public function __construct(?array $array = null) { $this->array = $array ?: []; } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php index d1e4d23f9dc53..e3850cca6e32f 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php @@ -39,4 +39,9 @@ public function __get(string $property) return 'constant value'; } } + + public function __isset(string $property) + { + return \in_array($property, ['magicProperty', 'constantMagicProperty'], true); + } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php index bc7ba3d9ffc4a..2e03358597424 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php @@ -19,7 +19,7 @@ class TraversableArrayObject implements \ArrayAccess, \IteratorAggregate, \Count { private $array; - public function __construct(array $array = null) + public function __construct(?array $array = null) { $this->array = $array ?: []; } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index fc9844944c66e..dc5c5500b18ea 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -990,12 +990,19 @@ public function testGetValuePropertyThrowsExceptionIfUninitializedWithLazyGhost( public function testGetValueGetterThrowsExceptionIfUninitializedWithLazyGhost() { + $lazyGhost = $this->createUninitializedObjectPropertyGhost(); + $this->expectException(UninitializedPropertyException::class); $this->expectExceptionMessage('The property "Symfony\Component\PropertyAccess\Tests\Fixtures\UninitializedObjectProperty::$privateUninitialized" is not readable because it is typed "DateTimeInterface". You should initialize it or declare a default value instead.'); + $this->propertyAccessor->getValue($lazyGhost, 'privateUninitialized'); + } + + public function testIsReadableWithMissingPropertyAndLazyGhost() + { $lazyGhost = $this->createUninitializedObjectPropertyGhost(); - $this->propertyAccessor->getValue($lazyGhost, 'privateUninitialized'); + $this->assertFalse($this->propertyAccessor->isReadable($lazyGhost, 'dummy')); } private function createUninitializedObjectPropertyGhost(): UninitializedObjectProperty diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index bc0bc77342f0e..a66a8dbda4c19 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -274,14 +274,12 @@ public function getReadInfo(string $class, string $property, array $context = [] return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisiblityForMethod($method), $method->isStatic(), false); } - if ($allowMagicGet && $reflClass->hasMethod('__get') && ($reflClass->getMethod('__get')->getModifiers() & $this->methodReflectionFlags)) { - return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, false); + if ($allowMagicGet && $reflClass->hasMethod('__get') && (($r = $reflClass->getMethod('__get'))->getModifiers() & $this->methodReflectionFlags)) { + return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, $r->returnsReference()); } - if ($hasProperty && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { - $reflProperty = $reflClass->getProperty($property); - - return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($reflProperty), $reflProperty->isStatic(), true); + if ($hasProperty && (($r = $reflClass->getProperty($property))->getModifiers() & $this->propertyReflectionFlags)) { + return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($r), $r->isStatic(), true); } if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) { @@ -642,7 +640,7 @@ private function getMutatorMethod(string $class, string $property): ?array continue; } - // Parameter can be optional to allow things like: method(array $foo = null) + // Parameter can be optional to allow things like: method(?array $foo = null) if ($reflectionMethod->getNumberOfParameters() >= 1) { return [$reflectionMethod, $prefix]; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index b6cd09670ecfd..252df9914f683 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -53,7 +53,6 @@ public static function invalidTypesProvider() return [ 'pub' => ['pub', null, null], 'stat' => ['stat', null, null], - 'foo' => ['foo', 'Foo.', null], 'bar' => ['bar', 'Bar.', null], ]; } @@ -68,10 +67,20 @@ public function testInvalid($property, $shortDescription, $longDescription) $this->assertSame($longDescription, $this->extractor->getLongDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property)); } + /** + * @group legacy + */ + public function testEmptyParamAnnotation() + { + $this->assertNull($this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', 'foo')); + $this->assertSame('Foo.', $this->extractor->getShortDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', 'foo')); + $this->assertNull($this->extractor->getLongDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', 'foo')); + } + /** * @dataProvider typesWithNoPrefixesProvider */ - public function testExtractTypesWithNoPrefixes($property, array $type = null) + public function testExtractTypesWithNoPrefixes($property, ?array $type = null) { $noPrefixExtractor = new PhpDocExtractor(null, [], [], []); @@ -193,7 +202,7 @@ public static function provideCollectionTypes() /** * @dataProvider typesWithCustomPrefixesProvider */ - public function testExtractTypesWithCustomPrefixes($property, array $type = null) + public function testExtractTypesWithCustomPrefixes($property, ?array $type = null) { $customExtractor = new PhpDocExtractor(null, ['add', 'remove'], ['is', 'can']); @@ -392,7 +401,7 @@ public function testUnknownPseudoType() /** * @dataProvider constructorTypesProvider */ - public function testExtractConstructorTypes($property, array $type = null) + public function testExtractConstructorTypes($property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index fdd90c7b43c80..3c32c84d14fd8 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -44,7 +44,7 @@ protected function setUp(): void /** * @dataProvider typesProvider */ - public function testExtract($property, array $type = null) + public function testExtract($property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); } @@ -75,7 +75,7 @@ public function testInvalid($property) /** * @dataProvider typesWithNoPrefixesProvider */ - public function testExtractTypesWithNoPrefixes($property, array $type = null) + public function testExtractTypesWithNoPrefixes($property, ?array $type = null) { $noPrefixExtractor = new PhpStanExtractor([], [], []); @@ -130,7 +130,7 @@ public static function typesProvider() /** * @dataProvider provideCollectionTypes */ - public function testExtractCollection($property, array $type = null) + public function testExtractCollection($property, ?array $type = null) { $this->testExtract($property, $type); } @@ -186,7 +186,7 @@ public static function provideCollectionTypes() /** * @dataProvider typesWithCustomPrefixesProvider */ - public function testExtractTypesWithCustomPrefixes($property, array $type = null) + public function testExtractTypesWithCustomPrefixes($property, ?array $type = null) { $customExtractor = new PhpStanExtractor(['add', 'remove'], ['is', 'can']); @@ -344,7 +344,7 @@ public static function propertiesParentTypeProvider(): array /** * @dataProvider constructorTypesProvider */ - public function testExtractConstructorTypes($property, array $type = null) + public function testExtractConstructorTypes($property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } @@ -459,7 +459,7 @@ public static function intRangeTypeProvider(): array /** * @dataProvider php80TypesProvider */ - public function testExtractPhp80Type(string $class, $property, array $type = null) + public function testExtractPhp80Type(string $class, $property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes($class, $property, [])); } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index ec3bb8da4e200..c76f7a7c8b296 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -194,7 +194,7 @@ public function getA() * * @param ParentDummy|null $parent */ - public function setB(ParentDummy $parent = null) + public function setB(?ParentDummy $parent = null) { } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures/ExtendedRoute.php b/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures/ExtendedRoute.php index 72232cbf6d50a..dca36a7ea2a56 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures/ExtendedRoute.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures/ExtendedRoute.php @@ -7,7 +7,7 @@ #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class ExtendedRoute extends Route { - public function __construct(array|string $path = null, ?string $name = null, array $defaults = []) + public function __construct(array|string|null $path = null, ?string $name = null, array $defaults = []) { parent::__construct("/{section<(foo|bar|baz)>}" . $path, $name, [], [], array_merge(['section' => 'foo'], $defaults)); } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/RedirectableUrlMatcher.php b/src/Symfony/Component/Routing/Tests/Fixtures/RedirectableUrlMatcher.php index c5f49a83057c3..6c1dd651855ae 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/RedirectableUrlMatcher.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/RedirectableUrlMatcher.php @@ -19,7 +19,7 @@ */ class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface { - public function redirect(string $path, string $route, string $scheme = null): array + public function redirect(string $path, string $route, ?string $scheme = null): array { return [ '_controller' => 'Some controller reference...', diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/TraceableAttributeClassLoader.php b/src/Symfony/Component/Routing/Tests/Fixtures/TraceableAttributeClassLoader.php index 1e2a2637dee8c..36b7619c7df6d 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/TraceableAttributeClassLoader.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/TraceableAttributeClassLoader.php @@ -20,7 +20,7 @@ final class TraceableAttributeClassLoader extends AttributeClassLoader /** @var list */ public array $foundClasses = []; - public function load(mixed $class, string $type = null): RouteCollection + public function load(mixed $class, ?string $type = null): RouteCollection { if (!is_string($class)) { throw new \InvalidArgumentException(sprintf('Expected string, got "%s"', get_debug_type($class))); diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php index 1bfe5d4737058..b2df2c9b9061a 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php @@ -130,7 +130,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $accessorOrMutator = preg_match('/^(get|is|has|set)(.+)$/i', $method->name, $matches); if ($accessorOrMutator) { - $attributeName = lcfirst($matches[2]); + $attributeName = $reflectionClass->hasProperty($method->name) ? $method->name : lcfirst($matches[2]); if (isset($attributesMetadata[$attributeName])) { $attributeMetadata = $attributesMetadata[$attributeName]; diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index 807a43ac6d306..04b51ffade390 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -90,17 +90,25 @@ protected function extractAttributes(object $object, ?string $format = null, arr if (str_starts_with($name, 'get') || str_starts_with($name, 'has') || str_starts_with($name, 'can')) { // getters, hassers and canners - $attributeName = substr($name, 3); + $attributeName = $name; if (!$reflClass->hasProperty($attributeName)) { - $attributeName = lcfirst($attributeName); + $attributeName = substr($attributeName, 3); + + if (!$reflClass->hasProperty($attributeName)) { + $attributeName = lcfirst($attributeName); + } } } elseif (str_starts_with($name, 'is')) { // issers - $attributeName = substr($name, 2); + $attributeName = $name; if (!$reflClass->hasProperty($attributeName)) { - $attributeName = lcfirst($attributeName); + $attributeName = substr($attributeName, 2); + + if (!$reflClass->hasProperty($attributeName)) { + $attributeName = lcfirst($attributeName); + } } } diff --git a/src/Symfony/Component/Serializer/Tests/Annotation/ContextTest.php b/src/Symfony/Component/Serializer/Tests/Annotation/ContextTest.php index 7efe8dda598d2..9584d6f1b5e51 100644 --- a/src/Symfony/Component/Serializer/Tests/Annotation/ContextTest.php +++ b/src/Symfony/Component/Serializer/Tests/Annotation/ContextTest.php @@ -133,7 +133,7 @@ public static function provideValidInputs(): iterable DUMP ]; - yield 'named arguemnts: with groups option as array' => [ + yield 'named arguments: with groups option as array' => [ fn () => new Context(context: ['foo' => 'bar'], groups: ['a', 'b']), << $this->foo, @@ -33,7 +33,7 @@ public function normalize(NormalizerInterface $normalizer, string $format = null ]; } - public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, string $format = null, array $context = []): void + public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, ?string $format = null, array $context = []): void { $this->foo = $data['foo']; $this->bar = $data['bar']; diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php index 15bcc6e6bec7f..98e0fa06fcd77 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php @@ -22,7 +22,7 @@ class DummyString implements DenormalizableInterface /** @var string $value */ public $value; - public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = []): void + public function denormalize(DenormalizerInterface $denormalizer, $data, ?string $format = null, array $context = []): void { $this->value = $data; } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopeNormalizer.php b/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopeNormalizer.php index 021c22a04c0ef..4acfdf8304743 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopeNormalizer.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopeNormalizer.php @@ -20,7 +20,7 @@ class EnvelopeNormalizer implements NormalizerInterface { private $serializer; - public function normalize($envelope, string $format = null, array $context = []): array + public function normalize($envelope, ?string $format = null, array $context = []): array { $xmlContent = $this->serializer->serialize($envelope->message, 'xml'); @@ -38,7 +38,7 @@ public function getSupportedTypes(?string $format): array ]; } - public function supportsNormalization($data, string $format = null, array $context = []): bool + public function supportsNormalization($data, ?string $format = null, array $context = []): bool { return $data instanceof EnvelopeObject; } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopedMessageNormalizer.php b/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopedMessageNormalizer.php index 5d48f4569cb0a..812dbf015730a 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopedMessageNormalizer.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopedMessageNormalizer.php @@ -18,7 +18,7 @@ */ class EnvelopedMessageNormalizer implements NormalizerInterface { - public function normalize($message, string $format = null, array $context = []): array + public function normalize($message, ?string $format = null, array $context = []): array { return [ 'text' => $message->text, @@ -32,7 +32,7 @@ public function getSupportedTypes(?string $format): array ]; } - public function supportsNormalization($data, string $format = null, array $context = []): bool + public function supportsNormalization($data, ?string $format = null, array $context = []): bool { return $data instanceof EnvelopedMessage; } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/FooInterfaceDummyDenormalizer.php b/src/Symfony/Component/Serializer/Tests/Fixtures/FooInterfaceDummyDenormalizer.php index a8c45373b70ee..0fa3c8202ac95 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/FooInterfaceDummyDenormalizer.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/FooInterfaceDummyDenormalizer.php @@ -15,7 +15,7 @@ final class FooInterfaceDummyDenormalizer implements DenormalizerInterface { - public function denormalize(mixed $data, string $type, string $format = null, array $context = []): array + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): array { $result = []; foreach ($data as $foo) { @@ -27,7 +27,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar return $result; } - public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { if (str_ends_with($type, '[]')) { $className = substr($type, 0, -2); diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/FormatAndContextAwareNormalizer.php b/src/Symfony/Component/Serializer/Tests/Fixtures/FormatAndContextAwareNormalizer.php index 4042288450637..428c618a9ddd9 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/FormatAndContextAwareNormalizer.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/FormatAndContextAwareNormalizer.php @@ -15,7 +15,7 @@ class FormatAndContextAwareNormalizer extends ObjectNormalizer { - protected function isAllowedAttribute($classOrObject, string $attribute, string $format = null, array $context = []): bool + protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []): bool { return \in_array($attribute, ['foo', 'bar']) && 'foo_and_bar_included' === $format; } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/NormalizableTraversableDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/NormalizableTraversableDummy.php index d6b38b89af1dc..5165120c3bdb5 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/NormalizableTraversableDummy.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/NormalizableTraversableDummy.php @@ -18,7 +18,7 @@ class NormalizableTraversableDummy extends TraversableDummy implements NormalizableInterface, DenormalizableInterface { - public function normalize(NormalizerInterface $normalizer, string $format = null, array $context = []): array|string|int|float|bool + public function normalize(NormalizerInterface $normalizer, ?string $format = null, array $context = []): array|string|int|float|bool { return [ 'foo' => 'normalizedFoo', @@ -26,7 +26,7 @@ public function normalize(NormalizerInterface $normalizer, string $format = null ]; } - public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, string $format = null, array $context = []): void + public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, ?string $format = null, array $context = []): void { } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php index e8c64f57752dd..41da0eac8a999 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php @@ -24,7 +24,7 @@ public function __construct() { } - public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = []): void + public function denormalize(DenormalizerInterface $denormalizer, $data, ?string $format = null, array $context = []): void { throw new NotNormalizableValueException(); } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodDummy.php new file mode 100644 index 0000000000000..89c8fcb9c399c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodDummy.php @@ -0,0 +1,48 @@ + + * + * 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; + +class SamePropertyAsMethodDummy +{ + private $freeTrial; + private $hasSubscribe; + private $getReady; + private $isActive; + + public function __construct($freeTrial, $hasSubscribe, $getReady, $isActive) + { + $this->freeTrial = $freeTrial; + $this->hasSubscribe = $hasSubscribe; + $this->getReady = $getReady; + $this->isActive = $isActive; + } + + public function getFreeTrial() + { + return $this->freeTrial; + } + + public function hasSubscribe() + { + return $this->hasSubscribe; + } + + public function getReady() + { + return $this->getReady; + } + + public function isActive() + { + return $this->isActive; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithMethodSerializedNameDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithMethodSerializedNameDummy.php new file mode 100644 index 0000000000000..203118885853e --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithMethodSerializedNameDummy.php @@ -0,0 +1,54 @@ + + * + * 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\Annotation\SerializedName; + +class SamePropertyAsMethodWithMethodSerializedNameDummy +{ + private $freeTrial; + private $hasSubscribe; + private $getReady; + private $isActive; + + public function __construct($freeTrial, $hasSubscribe, $getReady, $isActive) + { + $this->freeTrial = $freeTrial; + $this->hasSubscribe = $hasSubscribe; + $this->getReady = $getReady; + $this->isActive = $isActive; + } + + #[SerializedName('free_trial_method')] + public function getFreeTrial() + { + return $this->freeTrial; + } + + #[SerializedName('has_subscribe_method')] + public function hasSubscribe() + { + return $this->hasSubscribe; + } + + #[SerializedName('get_ready_method')] + public function getReady() + { + return $this->getReady; + } + + #[SerializedName('is_active_method')] + public function isActive() + { + return $this->isActive; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithPropertySerializedNameDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithPropertySerializedNameDummy.php new file mode 100644 index 0000000000000..0b681934f2fab --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithPropertySerializedNameDummy.php @@ -0,0 +1,57 @@ + + * + * 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\Annotation\SerializedName; + +class SamePropertyAsMethodWithPropertySerializedNameDummy +{ + #[SerializedName('free_trial_property')] + private $freeTrial; + + #[SerializedName('has_subscribe_property')] + private $hasSubscribe; + + #[SerializedName('get_ready_property')] + private $getReady; + + #[SerializedName('is_active_property')] + private $isActive; + + public function __construct($freeTrial, $hasSubscribe, $getReady, $isActive) + { + $this->freeTrial = $freeTrial; + $this->hasSubscribe = $hasSubscribe; + $this->getReady = $getReady; + $this->isActive = $isActive; + } + + public function getFreeTrial() + { + return $this->freeTrial; + } + + public function hasSubscribe() + { + return $this->hasSubscribe; + } + + public function getReady() + { + return $this->getReady; + } + + public function isActive() + { + return $this->isActive; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/ScalarDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/ScalarDummy.php index 949407d116b68..50617f33eb5e0 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/ScalarDummy.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/ScalarDummy.php @@ -21,12 +21,12 @@ class ScalarDummy implements NormalizableInterface, DenormalizableInterface public $foo; public $xmlFoo; - public function normalize(NormalizerInterface $normalizer, string $format = null, array $context = []): array|string|int|float|bool + public function normalize(NormalizerInterface $normalizer, ?string $format = null, array $context = []): array|string|int|float|bool { return 'xml' === $format ? $this->xmlFoo : $this->foo; } - public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, string $format = null, array $context = []): void + public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, ?string $format = null, array $context = []): void { if ('xml' === $format) { $this->xmlFoo = $data; diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php b/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php index c2e77aa4baf50..1ba6884bde1d0 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php @@ -23,17 +23,17 @@ public function getSupportedTypes(?string $format): array return [StaticConstructorDummy::class]; } - protected function extractAttributes(object $object, string $format = null, array $context = []): array + protected function extractAttributes(object $object, ?string $format = null, array $context = []): array { return get_object_vars($object); } - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed + protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed { return $object->$attribute; } - protected function setAttributeValue(object $object, string $attribute, mixed $value, string $format = null, array $context = []): void + protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void { $object->$attribute = $value; } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 27db0aa73fad1..32e2adffc4afe 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -926,12 +926,12 @@ public function testProvidingContextCacheKeyGeneratesSameChildContextCacheKey() $normalizer = new class() extends AbstractObjectNormalizerDummy { public $childContextCacheKey; - protected function extractAttributes(object $object, string $format = null, array $context = []): array + protected function extractAttributes(object $object, ?string $format = null, array $context = []): array { return array_keys((array) $object); } - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed + protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed { return $object->{$attribute}; } @@ -966,12 +966,12 @@ public function testChildContextKeepsOriginalContextCacheKey() $normalizer = new class() extends AbstractObjectNormalizerDummy { public $childContextCacheKey; - protected function extractAttributes(object $object, string $format = null, array $context = []): array + protected function extractAttributes(object $object, ?string $format = null, array $context = []): array { return array_keys((array) $object); } - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed + protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed { return $object->{$attribute}; } @@ -1001,12 +1001,12 @@ public function testChildContextCacheKeyStaysFalseWhenOriginalCacheKeyIsFalse() $normalizer = new class() extends AbstractObjectNormalizerDummy { public $childContextCacheKey; - protected function extractAttributes(object $object, string $format = null, array $context = []): array + protected function extractAttributes(object $object, ?string $format = null, array $context = []): array { return array_keys((array) $object); } - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed + protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed { return $object->{$attribute}; } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index f500428cdfc29..10b8b3c96d1b4 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -42,6 +42,9 @@ use Symfony\Component\Serializer\Tests\Fixtures\Php74Dummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74DummyPrivate; use Symfony\Component\Serializer\Tests\Fixtures\Php80Dummy; +use Symfony\Component\Serializer\Tests\Fixtures\SamePropertyAsMethodDummy; +use Symfony\Component\Serializer\Tests\Fixtures\SamePropertyAsMethodWithMethodSerializedNameDummy; +use Symfony\Component\Serializer\Tests\Fixtures\SamePropertyAsMethodWithPropertySerializedNameDummy; use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder; use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait; @@ -850,6 +853,53 @@ public function testNormalizeStdClass() $this->assertSame(['baz' => 'baz'], $this->normalizer->normalize($o2)); } + + public function testSamePropertyAsMethod() + { + $object = new SamePropertyAsMethodDummy('free_trial', 'has_subscribe', 'get_ready', 'is_active'); + $expected = [ + 'freeTrial' => 'free_trial', + 'hasSubscribe' => 'has_subscribe', + 'getReady' => 'get_ready', + 'isActive' => 'is_active', + ]; + + $this->assertSame($expected, $this->normalizer->normalize($object)); + } + + public function testSamePropertyAsMethodWithPropertySerializedName() + { + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $this->normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + $this->normalizer->setSerializer($this->serializer); + + $object = new SamePropertyAsMethodWithPropertySerializedNameDummy('free_trial', 'has_subscribe', 'get_ready', 'is_active'); + $expected = [ + 'free_trial_property' => 'free_trial', + 'has_subscribe_property' => 'has_subscribe', + 'get_ready_property' => 'get_ready', + 'is_active_property' => 'is_active', + ]; + + $this->assertSame($expected, $this->normalizer->normalize($object)); + } + + public function testSamePropertyAsMethodWithMethodSerializedName() + { + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $this->normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + $this->normalizer->setSerializer($this->serializer); + + $object = new SamePropertyAsMethodWithMethodSerializedNameDummy('free_trial', 'has_subscribe', 'get_ready', 'is_active'); + $expected = [ + 'free_trial_method' => 'free_trial', + 'has_subscribe_method' => 'has_subscribe', + 'get_ready_method' => 'get_ready', + 'is_active_method' => 'is_active', + ]; + + $this->assertSame($expected, $this->normalizer->normalize($object)); + } } class ProxyObjectDummy extends ObjectDummy @@ -1024,6 +1074,11 @@ public function __get($name) return $this->foo = 123; } } + + public function __isset($name) + { + return 'foo' === $name; + } } class DummyWithConstructorObject diff --git a/src/Symfony/Component/Validator/Constraints/BicValidator.php b/src/Symfony/Component/Validator/Constraints/BicValidator.php index 045df64b544e2..6236da86d21f2 100644 --- a/src/Symfony/Component/Validator/Constraints/BicValidator.php +++ b/src/Symfony/Component/Validator/Constraints/BicValidator.php @@ -102,16 +102,6 @@ public function validate(mixed $value, Constraint $constraint) return; } - // first 4 letters must be alphabetic (bank code) - if (!ctype_alpha(substr($canonicalize, 0, 4))) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) - ->setCode(Bic::INVALID_BANK_CODE_ERROR) - ->addViolation(); - - return; - } - $bicCountryCode = substr($canonicalize, 4, 2); if (!isset(self::BIC_COUNTRY_TO_IBAN_COUNTRY_MAP[$bicCountryCode]) && !Countries::exists($bicCountryCode)) { $this->context->buildViolation($constraint->message) diff --git a/src/Symfony/Component/Validator/Constraints/File.php b/src/Symfony/Component/Validator/Constraints/File.php index c55ceece72699..863e306f281f4 100644 --- a/src/Symfony/Component/Validator/Constraints/File.php +++ b/src/Symfony/Component/Validator/Constraints/File.php @@ -41,6 +41,7 @@ class File extends Constraint self::EMPTY_ERROR => 'EMPTY_ERROR', self::TOO_LARGE_ERROR => 'TOO_LARGE_ERROR', self::INVALID_MIME_TYPE_ERROR => 'INVALID_MIME_TYPE_ERROR', + self::INVALID_EXTENSION_ERROR => 'INVALID_EXTENSION_ERROR', self::FILENAME_TOO_LONG => 'FILENAME_TOO_LONG', ]; diff --git a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php index 1e692fe682a21..b0eefd84cb82f 100644 --- a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php @@ -43,12 +43,12 @@ public function validate(mixed $value, Constraint $constraint) $collectionElements = []; $normalizer = $this->getNormalizer($constraint); foreach ($value as $element) { + $element = $normalizer($element); + if ($fields && !$element = $this->reduceElementKeys($fields, $element)) { continue; } - $element = $normalizer($element); - if (\in_array($element, $collectionElements, true)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf index 6ac303a778fa9..dfd398ae95a4f 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf @@ -136,7 +136,7 @@ This value is not a valid IP address. - هذه القيمة ليست عنوان IP صالحًا. + هذا ليس عنوان IP صحيح. This value is not a valid language. @@ -192,7 +192,7 @@ No temporary folder was configured in php.ini, or the configured folder does not exist. - لم يتم تكوين مجلد مؤقت في ملف php.ini، أو المجلد المعد لا يوجد. + لم يتم تكوين مجلد مؤقت في ملف php.ini. Cannot write temporary file to disk. @@ -224,7 +224,7 @@ This value is not a valid International Bank Account Number (IBAN). - هذه القيمة ليست رقم حساب بنكي دولي (IBAN) صالحًا. + هذه القيمة ليست رقم حساب بنكي دولي (IBAN) صالحًا. This value is not a valid ISBN-10. @@ -312,7 +312,7 @@ This value is not a valid Business Identifier Code (BIC). - هذه القيمة ليست رمز معرف الأعمال (BIC) صالحًا. + هذه القيمة ليست رمز معرف أعمال (BIC) صالحًا. Error @@ -320,7 +320,7 @@ This value is not a valid UUID. - هذه القيمة ليست UUID صالحًا. + هذه القيمة ليست UUID صالحًا. This value should be a multiple of {{ compared_value }}. @@ -432,11 +432,11 @@ The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}. - تم اكتشاف ترميز الأحرف غير صالح ({{ detected }}). الترميزات المسموح بها هي {{ encodings }}. + تم اكتشاف ترميز أحرف غير صالح ({{ detected }}). الترميزات المسموح بها هي {{ encodings }}. This value is not a valid MAC address. - هذه القيمة ليست عنوان MAC صالحًا. + هذه القيمة ليست عنوان MAC صالحًا. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf index f7677fabfb89a..db927e1d51e65 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf @@ -136,7 +136,7 @@ This value is not a valid IP address. - Αυτή η τιμή δεν είναι έγκυρη διεύθυνση IP. + Αυτή η IP διεύθυνση δεν είναι έγκυρη. This value is not a valid language. @@ -192,7 +192,7 @@ No temporary folder was configured in php.ini, or the configured folder does not exist. - Δεν ρυθμίστηκε προσωρινός φάκελος στο php.ini, ή ο ρυθμισμένος φάκελος δεν υπάρχει. + Δεν έχει ρυθμιστεί προσωρινός φάκελος στο php.ini, ή ο ρυθμισμένος φάκελος δεν υπάρχει. Cannot write temporary file to disk. @@ -224,7 +224,7 @@ This value is not a valid International Bank Account Number (IBAN). - Αυτή η τιμή δεν είναι έγκυρος Διεθνής Αριθμός Τραπεζικού Λογαριασμού (IBAN). + Αυτός δεν είναι έγκυρος διεθνής αριθμός τραπεζικού λογαριασμού (IBAN). This value is not a valid ISBN-10. @@ -312,7 +312,7 @@ This value is not a valid Business Identifier Code (BIC). - Αυτή η τιμή δεν είναι έγκυρος Κωδικός Ταυτοποίησης Επιχείρησης (BIC). + Αυτός ο αριθμός δεν είναι έγκυρος Κωδικός Ταυτοποίησης Επιχείρησης (BIC). Error @@ -320,7 +320,7 @@ This value is not a valid UUID. - Αυτή η τιμή δεν είναι έγκυρη UUID. + Αυτός ο αριθμός δεν είναι έγκυρη UUID. This value should be a multiple of {{ compared_value }}. @@ -436,7 +436,7 @@ This value is not a valid MAC address. - Αυτή η τιμή δεν είναι έγκυρη διεύθυνση MAC. + Αυτός ο αριθμός δεν είναι έγκυρη διεύθυνση MAC. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php index 699979f3e6f63..8b3c815423a48 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php @@ -190,7 +190,6 @@ public function testValidBics($bic) public static function getValidBics() { - // http://formvalidation.io/validators/bic/ return [ ['ASPKAT2LXXX'], ['ASPKAT2L'], @@ -198,6 +197,7 @@ public static function getValidBics() ['UNCRIT2B912'], ['DABADKKK'], ['RZOOAT2L303'], + ['1SBACNBXSHA'], ]; } @@ -241,11 +241,6 @@ public static function getInvalidBics() ['ASPKAT2LX', Bic::INVALID_LENGTH_ERROR], ['ASPKAT2LXXX1', Bic::INVALID_LENGTH_ERROR], ['DABADKK', Bic::INVALID_LENGTH_ERROR], - ['1SBACNBXSHA', Bic::INVALID_BANK_CODE_ERROR], - ['RZ00AT2L303', Bic::INVALID_BANK_CODE_ERROR], - ['D2BACNBXSHA', Bic::INVALID_BANK_CODE_ERROR], - ['DS3ACNBXSHA', Bic::INVALID_BANK_CODE_ERROR], - ['DSB4CNBXSHA', Bic::INVALID_BANK_CODE_ERROR], ['DEUT12HH', Bic::INVALID_COUNTRY_CODE_ERROR], ['DSBAC6BXSHA', Bic::INVALID_COUNTRY_CODE_ERROR], ['DSBA5NBXSHA', Bic::INVALID_COUNTRY_CODE_ERROR], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php index 6894d2f95e5e4..d15e41660b7d3 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php @@ -56,14 +56,26 @@ public static function provideNonSuspiciousStrings(): iterable /** * @dataProvider provideSuspiciousStrings */ - public function testSuspiciousStrings(string $string, array $options, string $errorCode, string $errorMessage) + public function testSuspiciousStrings(string $string, array $options, array $errors) { $this->validator->validate($string, new NoSuspiciousCharacters($options)); - $this->buildViolation($errorMessage) - ->setCode($errorCode) - ->setParameter('{{ value }}', '"'.$string.'"') - ->assertRaised(); + $violations = null; + + foreach ($errors as $code => $message) { + if (null === $violations) { + $violations = $this->buildViolation($message); + } else { + $violations = $violations->buildNextViolation($message); + } + + $violations = $violations + ->setCode($code) + ->setParameter('{{ value }}', '"'.$string.'"') + ; + } + + $violations->assertRaised(); } public static function provideSuspiciousStrings(): iterable @@ -71,8 +83,7 @@ public static function provideSuspiciousStrings(): iterable yield 'Fails RESTRICTION_LEVEL check because of character outside ASCII range' => [ 'à', ['restrictionLevel' => NoSuspiciousCharacters::RESTRICTION_LEVEL_ASCII], - NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR, - 'This value contains characters that are not allowed by the current restriction-level.', + [NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR => 'This value contains characters that are not allowed by the current restriction-level.'], ]; yield 'Fails RESTRICTION_LEVEL check because of mixed-script string' => [ @@ -81,8 +92,7 @@ public static function provideSuspiciousStrings(): iterable 'restrictionLevel' => NoSuspiciousCharacters::RESTRICTION_LEVEL_SINGLE_SCRIPT, 'locales' => ['en', 'zh_Hant_TW'], ], - NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR, - 'This value contains characters that are not allowed by the current restriction-level.', + [NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR => 'This value contains characters that are not allowed by the current restriction-level.'], ]; yield 'Fails RESTRICTION_LEVEL check because RESTRICTION_LEVEL_HIGH disallows Armenian script' => [ @@ -91,8 +101,7 @@ public static function provideSuspiciousStrings(): iterable 'restrictionLevel' => NoSuspiciousCharacters::RESTRICTION_LEVEL_HIGH, 'locales' => ['en', 'hy_AM'], ], - NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR, - 'This value contains characters that are not allowed by the current restriction-level.', + [NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR => 'This value contains characters that are not allowed by the current restriction-level.'], ]; yield 'Fails RESTRICTION_LEVEL check because RESTRICTION_LEVEL_MODERATE disallows Greek script' => [ @@ -101,8 +110,7 @@ public static function provideSuspiciousStrings(): iterable 'restrictionLevel' => NoSuspiciousCharacters::RESTRICTION_LEVEL_MODERATE, 'locales' => ['en', 'el_GR'], ], - NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR, - 'This value contains characters that are not allowed by the current restriction-level.', + [NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR => 'This value contains characters that are not allowed by the current restriction-level.'], ]; yield 'Fails RESTRICTION_LEVEL check because of characters missing from the configured locales’ scripts' => [ @@ -111,8 +119,7 @@ public static function provideSuspiciousStrings(): iterable 'restrictionLevel' => NoSuspiciousCharacters::RESTRICTION_LEVEL_MINIMAL, 'locales' => ['en'], ], - NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR, - 'This value contains characters that are not allowed by the current restriction-level.', + [NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR => 'This value contains characters that are not allowed by the current restriction-level.'], ]; yield 'Fails INVISIBLE check because of duplicated non-spacing mark' => [ @@ -120,8 +127,7 @@ public static function provideSuspiciousStrings(): iterable [ 'checks' => NoSuspiciousCharacters::CHECK_INVISIBLE, ], - NoSuspiciousCharacters::INVISIBLE_ERROR, - 'Using invisible characters is not allowed.', + [NoSuspiciousCharacters::INVISIBLE_ERROR => 'Using invisible characters is not allowed.'], ]; yield 'Fails MIXED_NUMBERS check because of different numbering systems' => [ @@ -129,8 +135,7 @@ public static function provideSuspiciousStrings(): iterable [ 'checks' => NoSuspiciousCharacters::CHECK_MIXED_NUMBERS, ], - NoSuspiciousCharacters::MIXED_NUMBERS_ERROR, - 'Mixing numbers from different scripts is not allowed.', + [NoSuspiciousCharacters::MIXED_NUMBERS_ERROR => 'Mixing numbers from different scripts is not allowed.'], ]; yield 'Fails HIDDEN_OVERLAY check because of hidden combining character' => [ @@ -138,8 +143,19 @@ public static function provideSuspiciousStrings(): iterable [ 'checks' => NoSuspiciousCharacters::CHECK_HIDDEN_OVERLAY, ], - NoSuspiciousCharacters::HIDDEN_OVERLAY_ERROR, - 'Using hidden overlay characters is not allowed.', + [NoSuspiciousCharacters::HIDDEN_OVERLAY_ERROR => 'Using hidden overlay characters is not allowed.'], + ]; + + yield 'Fails both HIDDEN_OVERLAY and RESTRICTION_LEVEL checks' => [ + 'i̇', + [ + 'checks' => NoSuspiciousCharacters::CHECK_HIDDEN_OVERLAY, + 'restrictionLevel' => NoSuspiciousCharacters::RESTRICTION_LEVEL_ASCII, + ], + [ + NoSuspiciousCharacters::RESTRICTION_LEVEL_ERROR => 'This value contains characters that are not allowed by the current restriction-level.', + NoSuspiciousCharacters::HIDDEN_OVERLAY_ERROR => 'Using hidden overlay characters is not allowed.', + ], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php index 35bde67d9bd3f..3c2dd9f21c98f 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Dummy\DummyClassOne; class UniqueValidatorTest extends ConstraintValidatorTestCase { @@ -283,6 +284,36 @@ public static function getInvalidCollectionValues(): array ], ]; } + + public function testArrayOfObjectsUnique() + { + $array = [ + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + ]; + + $array[0]->code = '1'; + $array[1]->code = '2'; + $array[2]->code = '3'; + + $this->validator->validate( + $array, + new Unique( + normalizer: [self::class, 'normalizeDummyClassOne'], + fields: 'code' + ) + ); + + $this->assertNoViolation(); + } + + public static function normalizeDummyClassOne(DummyClassOne $obj): array + { + return [ + 'code' => $obj->code, + ]; + } } class CallableClass diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithRequiredArgument.php b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithRequiredArgument.php index 3048ae5ce1b99..f8abc8a563f52 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithRequiredArgument.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithRequiredArgument.php @@ -20,7 +20,7 @@ final class ConstraintWithRequiredArgument extends Constraint public string $requiredArg; #[HasNamedArguments] - public function __construct(string $requiredArg, array $groups = null, mixed $payload = null) + public function __construct(string $requiredArg, ?array $groups = null, mixed $payload = null) { parent::__construct([], $groups, $payload); diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php b/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php index ee5b9c22db3e9..dab811dc2182d 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php @@ -19,7 +19,7 @@ class CustomArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable { private $array; - public function __construct(array $array = null) + public function __construct(?array $array = null) { $this->array = $array ?: []; } diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php index 4adb9bc9fe88d..f3da9f920af78 100644 --- a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php @@ -128,10 +128,16 @@ public static function castType(\ReflectionType $c, array $a, Stub $stub, bool $ */ public static function castAttribute(\ReflectionAttribute $c, array $a, Stub $stub, bool $isNested) { - self::addMap($a, $c, [ + $map = [ 'name' => 'getName', 'arguments' => 'getArguments', - ]); + ]; + + if (\PHP_VERSION_ID >= 80400) { + unset($map['name']); + } + + self::addMap($a, $c, $map); return $a; } @@ -407,7 +413,7 @@ public static function getSignature(array $a) if (!$type instanceof \ReflectionNamedType) { $signature .= $type.' '; } else { - if (!$param->isOptional() && $param->allowsNull() && 'mixed' !== $type->getName()) { + if ($param->allowsNull() && 'mixed' !== $type->getName()) { $signature .= '?'; } $signature .= substr(strrchr('\\'.$type->getName(), '\\'), 1).' '; diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index 2602c36051614..eac092f29f0d6 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -534,13 +534,14 @@ class: "Symfony\Component\VarDumper\Tests\Caster\ReflectionCasterTest" public function testReflectionClassWithAttribute() { $var = new \ReflectionClass(LotsOfAttributes::class); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" arguments: [] } ] @@ -553,14 +554,15 @@ public function testReflectionClassWithAttribute() public function testReflectionMethodWithAttribute() { $var = new \ReflectionMethod(LotsOfAttributes::class, 'someMethod'); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" arguments: array:1 [ 0 => "two" ] @@ -575,14 +577,15 @@ public function testReflectionMethodWithAttribute() public function testReflectionPropertyWithAttribute() { $var = new \ReflectionProperty(LotsOfAttributes::class, 'someProperty'); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" arguments: array:2 [ 0 => "one" "extra" => "hello" @@ -597,8 +600,9 @@ public function testReflectionPropertyWithAttribute() public function testReflectionClassConstantWithAttribute() { $var = new \ReflectionClassConstant(LotsOfAttributes::class, 'SOME_CONSTANT'); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute" arguments: array:1 [ 0 => "one" ] } 1 => ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute" arguments: array:1 [ 0 => "two" ] @@ -626,14 +630,15 @@ public function testReflectionClassConstantWithAttribute() public function testReflectionParameterWithAttribute() { $var = new \ReflectionParameter([LotsOfAttributes::class, 'someMethod'], 'someParameter'); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" arguments: array:1 [ 0 => "three" ] diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php index 8b3e12b5c1d72..cf0bc7338326d 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php @@ -155,7 +155,7 @@ public function testClassStub() $expectedDump = <<<'EODUMP' array:1 [ - 0 => "hello(?stdClass $a, stdClass $b = null)" + 0 => "hello(?stdClass $a, ?stdClass $b = null)" ] EODUMP; diff --git a/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php b/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php index 1e0e6bda05751..824c9f10df606 100644 --- a/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php @@ -90,7 +90,7 @@ public function testGet() +foo: ""…3 +"bar": "bar" } - "closure" => Closure(\$a, PDO &\$b = null) {#%d + "closure" => Closure(\$a, ?PDO &\$b = null) {#%d class: "Symfony\Component\VarDumper\Tests\Dumper\CliDumperTest" this: Symfony\Component\VarDumper\Tests\Dumper\CliDumperTest {#%d …} file: "%s%eTests%eFixtures%edumb-var.php" diff --git a/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php b/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php index c31d07d2f26a5..9b914ad6d3c37 100644 --- a/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php @@ -84,7 +84,7 @@ public function testGet() +foo: "foo" +"bar": "bar" } - "closure" => Closure(\$a, PDO &\$b = null) {#%d + "closure" => Closure(\$a, ?PDO &\$b = null) {#%d class: "Symfony\Component\VarDumper\Tests\Dumper\HtmlDumperTest" this: stop(); + } + } }