diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 0df7764a3ed2..575f27685e01 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 33b87c42af2d..f90caa71d3d8 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 723085c67be5..2e0f70ad13e6 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 c4a195380c0a..f676651321be 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 0091db8b543c..eae0d1cafe80 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 d277992bd5a1..0537946986d2 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 783cf5a5524e..92e750929f41 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 7a12292c6546..c5412dd71ba5 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 f660a8060f96..a2259fc1304e 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 19a9bdd5125d..c12f1150b698 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 0bd7d5918983..99b7abbee3f1 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 cb5a60e07986..17b28fc9ab8d 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 40880df4760a..179f8e981dd3 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 a71bf3a7610e..b57bc0cdbec6 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 29cb4875d2a5..ad352160822a 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 ffa47c3845fc..aedd4a86fd11 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 2311df0fcf36..f70d60429e90 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 a707c02662fd..68387298270a 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 02ecd32e757f..3436cf417caf 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 650e6792dd19..1cd8523b680f 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 e2793a9e7b7f..e826d6bdcff9 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 e24031829305..7e30456900ea 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 000000000000..775276d84a87 --- /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 630bfb7cd300..2856816d187a 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 667bc4f93da0..2c0562e4066a 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 65bc90cd8487..bee9a14c8d25 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 13d8fd3ba330..c79b739594c3 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 f0d97724a4e3..0f7337fe6e91 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 8d315ffd2c51..0e3f2de0b5ec 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 59ab11b0f3c5..e41c0d10cc03 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 000000000000..d086d5b3e8a0 --- /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 da6526b6eb5d..6e06b075f27d 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 000000000000..389c6e1adf34 --- /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 67caa05a5bc3..abcf1bd9e974 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 e6ce07588f9d..84d9f596c189 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 18ad445c3ef5..cb33603f6cbb 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 bdf6d80bff44..9f62a684a38f 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 f4eec051c19a..b406292b44b3 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 6aad9e95b8c6..1f026dc504ad 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 728ea847f031..4af12e34c068 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 520977763f3a..b8f31ee41e94 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 3588a5b2a594..de8fea8ab425 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 f99a3f9eb519..4a7d87358aad 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 65437a63ec74..53f8bb7c3221 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 4f348895132c..98ee3a45a603 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 e775def68930..36f027f1dd9c 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 000000000000..8674c648e900 --- /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 807c8b3e2008..0659c968c8e7 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 76c69868cc49..2a1234fa7e26 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 a8e71068775b..a9ac5c0bff43 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 69ca09218812..0c7cc2a7b7ba 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 846c8fe64797..6084c42c77dd 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 8e910be31431..ba8d790571e8 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 3f580df3e30e..e4f355c04569 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 c61fb0be569b..d53fe9398d46 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 c096eaca3886..d82ce6c7024c 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 f216a8fba63e..2e13fc40e71b 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 a5f6330679ff..fd6d44e316af 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 2bac262ddb49..a9cf0dfcb4d2 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 0a25f882479d..48c66a8e41b1 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 cb8ed6a77514..bf4c1466c589 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 77b1e75bd49a..96bdc7c0de1a 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 66ad9ff416e2..f7ef667e769b 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 e86bf9e41ed1..5238e2fd8809 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 b37bfe5ed2d8..8be0323ae1a9 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 1fc0fa90165f..432f2ab12db9 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 7a5d5cdff68e..4464088c7810 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 e74e0263b985..4446a031e969 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 66372aa8a814..9372dbe5a0b0 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 266b539ef6b5..20cf7ef48291 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 a7493100c431..91ec7ea5f2c7 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 613d80cb1d3a..3e81f429622a 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 247588cd359a..182d52932f72 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 d4bae3ab5c4a..d1f4deb03a00 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 b371c08cf424..768353b04abd 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 ba9504ae1c66..a0e39cc46c85 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 cf437a653bd7..b6a2c03c8f7a 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 33fa3b455800..ef456a603763 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 9e5a3a56e1c3..626163d148b8 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 d3ed63106440..1b57d2b84809 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 e1508bf569b4..ad011fc49de4 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 da3d968d2d93..5a57faa67bfe 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 51d3f72a9fd2..138b9b0dcc22 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 8d556b8f37c9..5f5f0816c23c 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 000000000000..4fbeadc58112 --- /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 000000000000..57bb1ff1893e --- /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 000000000000..920b980e0f71 --- /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 407c90810b78..d67671ea1076 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 690032050605..a8d3540a115f 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 e05e347919d2..0de38fb2ed69 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 a68748b6c958..498dc560c3ed 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 3d59ecfecb7d..8644d7dad729 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 a08c102a4b9d..e57dad6cad97 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 b39902623adc..0ae1bff21d7c 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 e3030d3a1d55..5118e4ff3aaa 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 7e68e9f5c276..43affdabf722 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 72283878c1b0..0befccf4a1d1 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 a2fce4e1bb1e..42a8590ee70a 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 000000000000..b3d9bbc7f371 --- /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 a769b4efc79d..c9cf9eaaa0f6 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 33792585479a..15840fbd578d 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 c01ece62d544..7f8d65492b39 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 8e154d13f0b8..39bc172f8cb7 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 60f2d32770b0..4343c9fc313f 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 ca4074ca3229..ed9944d27a67 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 d1e4d23f9dc5..e3850cca6e32 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 bc7ba3d9ffc4..2e0335859742 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 fc9844944c66..dc5c5500b18e 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 bc0bc77342f0..a66a8dbda4c1 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 b6cd09670ecf..252df9914f68 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 fdd90c7b43c8..3c32c84d14fd 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 ec3bb8da4e20..c76f7a7c8b29 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 72232cbf6d50..dca36a7ea2a5 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 c5f49a83057c..6c1dd651855a 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 1e2a2637dee8..36b7619c7df6 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 1bfe5d473705..b2df2c9b9061 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 807a43ac6d30..04b51ffade39 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 7efe8dda598d..9584d6f1b5e5 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 15bcc6e6bec7..98e0fa06fcd7 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 021c22a04c0e..4acfdf830474 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 5d48f4569cb0..812dbf015730 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 a8c45373b70e..0fa3c8202ac9 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 404228845063..428c618a9ddd 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 d6b38b89af1d..5165120c3bdb 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 e8c64f57752d..41da0eac8a99 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 000000000000..89c8fcb9c399 --- /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 000000000000..203118885853 --- /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 000000000000..0b681934f2fa --- /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 949407d116b6..50617f33eb5e 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 c2e77aa4baf5..1ba6884bde1d 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 27db0aa73fad..32e2adffc4af 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 f500428cdfc2..10b8b3c96d1b 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 045df64b544e..6236da86d21f 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 c55ceece7269..863e306f281f 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 1e692fe682a2..b0eefd84cb82 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 6ac303a778fa..dfd398ae95a4 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 f7677fabfb89..db927e1d51e6 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 699979f3e6f6..8b3c815423a4 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 6894d2f95e5e..d15e41660b7d 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 35bde67d9bd3..3c2dd9f21c98 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 3048ae5ce1b9..f8abc8a563f5 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 ee5b9c22db3e..dab811dc2182 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 4adb9bc9fe88..f3da9f920af7 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 2602c3605161..eac092f29f0d 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 8b3e12b5c1d7..cf0bc7338326 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 1e0e6bda0575..824c9f10df60 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 c31d07d2f26a..9b914ad6d3c3 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(); + } + } } pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy