diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 9375b946fe856..c21cc6e59321f 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 3b17c22cbfcd5..1e4e77ab46ffc 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-7.0.md b/CHANGELOG-7.0.md index 2dd0c8b206cc2..394009335f611 100644 --- a/CHANGELOG-7.0.md +++ b/CHANGELOG-7.0.md @@ -7,6 +7,43 @@ in 7.0 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v7.0.0...v7.0.1 +* 7.0.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 #54333 [HttpFoundation] Allow array style callable setting for Request setFactory method (simbera) + * 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 #54265 [VarDumper] prevent error in value to Typed property must not be accessed before initialization (shakaran) + * 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) + * 7.0.5 (2024-03-04) * bug #54113 [AssetMapper] Throw exception in Javascript compiler when PCRE error (smnandre) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c4a195380c0a3..f676651321bef 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -80,11 +80,11 @@ The Symfony Connect username in parenthesis allows to get more information - Mathieu Santostefano (welcomattic) - Alexander Schranz (alexander-schranz) - Vasilij Duško (staff) + - Simon André (simonandre) - Sarah Khalil (saro0h) - Laurent VOULLEMIER (lvo) - Konstantin Kudryashov (everzet) - Tomasz Kowalczyk (thunderer) - - Simon André (simonandre) - Guilhem N (guilhemn) - Bilal Amarni (bamarni) - Eriksen Costa @@ -97,10 +97,10 @@ The Symfony Connect username in parenthesis allows to get more information - David Buchmann (dbu) - Andrej Hudec (pulzarraider) - Jáchym Toušek (enumag) + - Ruud Kamphuis (ruudk) - Christian Raue - Eric Clemmons (ericclemmons) - Denis (yethee) - - Ruud Kamphuis (ruudk) - Michel Weimerskirch (mweimerskirch) - Issei Murasawa (issei_m) - Douglas Greenshields (shieldo) @@ -129,8 +129,8 @@ The Symfony Connect username in parenthesis allows to get more information - Bart van den Burg (burgov) - Vasilij Dusko | CREATION - Jordan Alliot (jalliot) - - John Wards (johnwards) - Phil E. Taylor (philetaylor) + - John Wards (johnwards) - Théo FIDRY - Antoine Hérault (herzult) - Konstantin.Myakshin @@ -177,6 +177,7 @@ The Symfony Connect username in parenthesis allows to get more information - Maxime Helias (maxhelias) - Robert Schönthal (digitalkaoz) - Smaine Milianni (ismail1432) + - Michael Babker (mbabker) - François-Xavier de Guillebon (de-gui_f) - Maximilian Beckers (maxbeckers) - noniagriconomie @@ -188,7 +189,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jhonny Lidfors (jhonne) - Juti Noppornpitak (shiroyuki) - Gregor Harlan (gharlan) - - Michael Babker (mbabker) - Anthony MARTIN - Sebastian Hörl (blogsh) - Tigran Azatyan (tigranazatyan) @@ -231,6 +231,7 @@ The Symfony Connect username in parenthesis allows to get more information - George Mponos (gmponos) - Richard Shank (iampersistent) - Thomas Landauer (thomas-landauer) + - Roland Franssen :) - Romain Monteil (ker0x) - Sergey (upyx) - Marco Pivetta (ocramius) @@ -257,7 +258,6 @@ The Symfony Connect username in parenthesis allows to get more information - Artur Kotyrba - Wouter J - Tyson Andre - - Roland Franssen :) - GDIBass - Samuel NELA (snela) - Vincent AUBERT (vincent) @@ -733,6 +733,7 @@ The Symfony Connect username in parenthesis allows to get more information - Vadim Borodavko (javer) - Tavo Nieves J (tavoniievez) - Luc Vieillescazes (iamluc) + - Stiven Llupa (sllupa) - Erik Saunier (snickers) - François Dume (franek) - Jerzy Lekowski (jlekowski) @@ -1043,7 +1044,6 @@ The Symfony Connect username in parenthesis allows to get more information - Florian Pfitzer (marmelatze) - Ivan Grigoriev (greedyivan) - Johann Saunier (prophet777) - - Stiven Llupa (sllupa) - Kevin SCHNEKENBURGER - Fabien Salles (blacked) - Andreas Erhard (andaris) @@ -2117,6 +2117,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sébastien HOUZÉ - Mbechezi Nawo - wivaku + - Markus Reinhold - Jingyu Wang - steveYeah - Asrorbek (asrorbek) @@ -2523,6 +2524,7 @@ The Symfony Connect username in parenthesis allows to get more information - Roy de Vos Burchart - John Stevenson - everyx + - Richard Heine - Francisco Facioni (fran6co) - Stanislav Gamaiunov (happyproff) - Iwan van Staveren (istaveren) @@ -3193,6 +3195,7 @@ The Symfony Connect username in parenthesis allows to get more information - Daniele Cesarini (ijanki) - Ismail Asci (ismailasci) - Jeffrey Moelands (jeffreymoelands) + - Jakub Caban (lustmored) - Ondřej Mirtes (mirtes) - Paulius Jarmalavičius (pjarmalavicius) - Ramon Ornelas (ramonornela) @@ -3615,6 +3618,7 @@ The Symfony Connect username in parenthesis allows to get more information - Konrad - Kovacs Nicolas - eminjk + - Gálik Pál - craigmarvelley - Stano Turza - Antoine Leblanc diff --git a/composer.json b/composer.json index 7e66563bef783..008375cfbe7ec 100644 --- a/composer.json +++ b/composer.json @@ -133,7 +133,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 324cd029fd8b4..47986a28353fc 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -14,6 +14,7 @@ use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\FieldMapping; use Doctrine\ORM\Mapping\JoinColumnMapping; use Doctrine\ORM\Mapping\MappingException as LegacyMappingException; use Doctrine\Persistence\ManagerRegistry; @@ -129,8 +130,10 @@ public function guessMaxLength(string $class, string $property): ?ValueGuess if ($ret && isset($ret[0]->fieldMappings[$property]) && !$ret[0]->hasAssociation($property)) { $mapping = $ret[0]->getFieldMapping($property); - if (isset($mapping['length'])) { - return new ValueGuess($mapping['length'], Guess::HIGH_CONFIDENCE); + $length = $mapping instanceof FieldMapping ? $mapping->length : ($mapping['length'] ?? null); + + if (null !== $length) { + return new ValueGuess($length, Guess::HIGH_CONFIDENCE); } if (\in_array($ret[0]->getTypeOfField($property), [Types::DECIMAL, Types::FLOAT])) { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 783cf5a5524e9..92e750929f41e 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -1314,19 +1314,6 @@ public function testPassIdAndNameToView() $this->assertEquals('name', $view->vars['full_name']); } - public function testStripLeadingUnderscoresAndDigitsFromId() - { - $view = $this->factory->createNamed('_09name', static::TESTED_TYPE, null, [ - 'em' => 'default', - 'class' => self::SINGLE_IDENT_CLASS, - ]) - ->createView(); - - $this->assertEquals('name', $view->vars['id']); - $this->assertEquals('_09name', $view->vars['name']); - $this->assertEquals('_09name', $view->vars['full_name']); - } - public function testPassIdAndNameToViewWithParent() { $view = $this->factory->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE) diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index a5d1e1e09ba0c..be35a0a335994 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -29,7 +29,7 @@ "symfony/dependency-injection": "^6.4|^7.0", "symfony/doctrine-messenger": "^6.4|^7.0", "symfony/expression-language": "^6.4|^7.0", - "symfony/form": "^6.4|^7.0", + "symfony/form": "^6.4.6|^7.0.6", "symfony/http-kernel": "^6.4|^7.0", "symfony/lock": "^6.4|^7.0", "symfony/messenger": "^6.4|^7.0", @@ -53,7 +53,7 @@ "doctrine/orm": "<2.15", "symfony/cache": "<6.4", "symfony/dependency-injection": "<6.4", - "symfony/form": "<6.4", + "symfony/form": "<6.4.6|>=7,<7.0.6", "symfony/http-foundation": "<6.4", "symfony/http-kernel": "<6.4", "symfony/lock": "<6.4", diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php index f660a8060f962..a2259fc1304ec 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php @@ -525,7 +525,7 @@ public function testBaselineFileWriteError() $this->expectException(\ErrorException::class); $this->expectExceptionMessageMatches('/[Ff]ailed to open stream: Permission denied/'); - set_error_handler(static function (int $errno, string $errstr, string $errfile = null, int $errline = null): bool { + set_error_handler(static function (int $errno, string $errstr, ?string $errfile = null, ?int $errline = null): bool { if ($errno & (E_WARNING | E_WARNING)) { throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); } diff --git a/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/ServerRequest.php b/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/ServerRequest.php index 0bd7d5918983b..99b7abbee3f1b 100644 --- a/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/ServerRequest.php +++ b/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/ServerRequest.php @@ -26,10 +26,10 @@ class ServerRequest extends Message implements ServerRequestInterface public function __construct( string $version = '1.1', array $headers = [], - StreamInterface $body = null, + ?StreamInterface $body = null, private readonly string $requestTarget = '/', private readonly string $method = 'GET', - UriInterface|string $uri = null, + UriInterface|string|null $uri = null, private readonly array $server = [], private readonly array $cookies = [], private readonly array $query = [], diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig index cb5a60e079861..17b28fc9ab8d6 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig @@ -361,12 +361,12 @@ {# Help #} {%- block form_help -%} - {% set row_class = row_attr.class|default('') %} - {% set help_class = ' form-text' %} - {% if 'input-group' in row_class %} + {%- set row_class = row_attr.class|default('') -%} + {%- set help_class = ' form-text' -%} + {%- if 'input-group' in row_class -%} {#- Hack to properly display help with input group -#} - {% set help_class = ' input-group-text' %} - {% endif %} + {%- set help_class = ' input-group-text' -%} + {%- endif -%} {%- if help is not empty -%} {%- set help_attr = help_attr|merge({class: (help_attr.class|default('') ~ help_class ~ ' mb-0')|trim}) -%} {%- endif -%} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index 40880df4760ac..179f8e981dd32 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -154,7 +154,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($this->isNfs($realBuildDir)) { - $io->note('For better performances, you should move the cache and log directories to a non-shared folder of the VM.'); + $io->note('For better performance, you should move the cache and log directories to a non-shared folder of the VM.'); $fs->remove($realBuildDir); } else { $fs->rename($realBuildDir, $oldBuildDir); diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php index ebac3b3705261..cbffb5fcd7495 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 bb4f4cafc3996..6a006845f415d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -2074,6 +2074,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 57cc923345dff..2f70a3481de11 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 @@ -755,7 +755,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index 799cfb2900f9d..d46a92a02d585 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -212,6 +212,6 @@ ]) ->set('serializer.normalizer.backed_enum', BackedEnumNormalizer::class) - ->tag('serializer.normalizer', ['priority' => -915]) + ->tag('serializer.normalizer', ['priority' => -880]) ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php index a707c02662fd2..68387298270a3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php @@ -12,7 +12,7 @@ 'dsn' => 'smtp://example.com', 'envelope' => [ 'sender' => 'sender@example.org', - 'recipients' => ['redirected@example.org', 'redirected1@example.org'], + 'recipients' => ['redirected@example.org'], ], 'headers' => [ 'from' => 'from@example.org', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml index 02ecd32e757fc..3436cf417caf7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml @@ -12,8 +12,7 @@ sender@example.org - redirected@example.org - redirected1@example.org + redirected@example.org from@example.org bcc1@example.org diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml index 650e6792dd198..1cd8523b680f4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml @@ -14,8 +14,8 @@ smtp://example2.com sender@example.org - redirected@example.org - redirected1@example.org + redirected@example.org + redirected1@example.org from@example.org bcc1@example.org diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml index e2793a9e7b7f0..e826d6bdcff97 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml @@ -10,7 +10,6 @@ framework: sender: sender@example.org recipients: - redirected@example.org - - redirected1@example.org headers: from: from@example.org bcc: [bcc1@example.org, bcc2@example.org] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index bdda5134dd9d2..eede5ded314c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -355,7 +355,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); @@ -1989,21 +1989,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); @@ -2015,7 +2021,7 @@ public function testMailer(string $configFile, array $expectedTransports) $this->assertTrue($container->hasDefinition('mailer.envelope_listener')); $l = $container->getDefinition('mailer.envelope_listener'); $this->assertSame('sender@example.org', $l->getArgument(0)); - $this->assertSame(['redirected@example.org', 'redirected1@example.org'], $l->getArgument(1)); + $this->assertSame($expectedRecipients, $l->getArgument(1)); $this->assertEquals(new Reference('messenger.default_bus', ContainerInterface::NULL_ON_INVALID_REFERENCE), $container->getDefinition('mailer.mailer')->getArgument(1)); $this->assertTrue($container->hasDefinition('mailer.message_listener')); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TranslatableBackedEnum.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TranslatableBackedEnum.php new file mode 100644 index 0000000000000..775276d84a87d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TranslatableBackedEnum.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum TranslatableBackedEnum: string implements TranslatableInterface +{ + case Get = 'GET'; + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + return match ($this) { + self::Get => 'custom_get_string', + }; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php index 630bfb7cd3004..2856816d187a1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\TranslatableBackedEnum; + /** * @author Kévin Dunglas */ @@ -67,6 +69,15 @@ public static function provideNormalizersAndEncodersWithDefaultContextOption(): ['serializer.encoder.csv.alias'], ]; } + + public function testSerializeTranslatableBackedEnum() + { + static::bootKernel(['test_case' => 'Serializer']); + + $serializer = static::getContainer()->get('serializer.alias'); + + $this->assertEquals('GET', $serializer->serialize(TranslatableBackedEnum::Get, 'yaml')); + } } class Foo diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index 667bc4f93da04..2c0562e4066a3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -131,6 +131,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep // collect voters and access decision manager information if ($this->accessDecisionManager instanceof TraceableAccessDecisionManager) { $this->data['voter_strategy'] = $this->accessDecisionManager->getStrategy(); + $this->data['voters'] = []; foreach ($this->accessDecisionManager->getVoters() as $voter) { if ($voter instanceof TraceableVoter) { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index 1839080eeffbf..e4173b1fdc659 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 4fee819da9213..6e5e392dee542 100644 --- a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php @@ -372,10 +372,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/Traits/RedisProxiesTest.php b/src/Symfony/Component/Cache/Tests/Traits/RedisProxiesTest.php index 01d96fb9e0480..5c382fb89a71c 100644 --- a/src/Symfony/Component/Cache/Tests/Traits/RedisProxiesTest.php +++ b/src/Symfony/Component/Cache/Tests/Traits/RedisProxiesTest.php @@ -85,32 +85,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}); @@ -119,9 +118,8 @@ public function testRedis6Proxy($class, $stub) EOPHP; } - uksort($methods, 'strnatcmp'); - $proxy .= implode('', $methods)."}\n"; + usort($methods, 'strnatcmp'); - $this->assertStringEqualsFile(\dirname(__DIR__, 2)."/Traits/{$class}6Proxy.php", $proxy); + return implode("\n", $methods); } } diff --git a/src/Symfony/Component/Cache/Traits/Redis6Proxy.php b/src/Symfony/Component/Cache/Traits/Redis6Proxy.php index 59ab11b0f3c55..e41c0d10cc030 100644 --- a/src/Symfony/Component/Cache/Traits/Redis6Proxy.php +++ b/src/Symfony/Component/Cache/Traits/Redis6Proxy.php @@ -28,6 +28,7 @@ class Redis6Proxy extends \Redis implements ResetInterface, LazyObjectInterface use LazyProxyTrait { resetLazyObject as reset; } + use Redis6ProxyTrait; private const LAZY_OBJECT_PROPERTY_SCOPES = []; @@ -96,11 +97,6 @@ public function bgrewriteaof(): \Redis|bool return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgrewriteaof(...\func_get_args()); } - public function waitaof($numlocal, $numreplicas, $timeout): \Redis|array|false - { - return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args()); - } - public function bitcount($key, $start = 0, $end = -1, $bybit = false): \Redis|false|int { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bitcount(...\func_get_args()); @@ -231,11 +227,6 @@ public function discard(): \Redis|bool return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->discard(...\func_get_args()); } - public function dump($key): \Redis|string - { - return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->dump(...\func_get_args()); - } - public function echo($str): \Redis|false|string { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->echo(...\func_get_args()); @@ -656,11 +647,6 @@ public function ltrim($key, $start, $end): \Redis|bool return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->ltrim(...\func_get_args()); } - public function mget($keys): \Redis|array - { - return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->mget(...\func_get_args()); - } - public function migrate($host, $port, $key, $dstdb, $timeout, $copy = false, $replace = false, #[\SensitiveParameter] $credentials = null): \Redis|bool { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->migrate(...\func_get_args()); diff --git a/src/Symfony/Component/Cache/Traits/Redis6ProxyTrait.php b/src/Symfony/Component/Cache/Traits/Redis6ProxyTrait.php new file mode 100644 index 0000000000000..d086d5b3e8a09 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/Redis6ProxyTrait.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +if (version_compare(phpversion('redis'), '6.0.2', '>')) { + /** + * @internal + */ + trait Redis6ProxyTrait + { + public function dump($key): \Redis|false|string + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->dump(...\func_get_args()); + } + + public function mget($keys): \Redis|array|false + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->mget(...\func_get_args()); + } + + public function waitaof($numlocal, $numreplicas, $timeout): \Redis|array|false + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args()); + } + } +} else { + /** + * @internal + */ + trait Redis6ProxyTrait + { + public function dump($key): \Redis|string + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->dump(...\func_get_args()); + } + + public function mget($keys): \Redis|array + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->mget(...\func_get_args()); + } + } +} diff --git a/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php b/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php index da6526b6eb5d1..6e06b075f27d7 100644 --- a/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php +++ b/src/Symfony/Component/Cache/Traits/RedisCluster6Proxy.php @@ -28,6 +28,7 @@ class RedisCluster6Proxy extends \RedisCluster implements ResetInterface, LazyOb use LazyProxyTrait { resetLazyObject as reset; } + use RedisCluster6ProxyTrait; private const LAZY_OBJECT_PROPERTY_SCOPES = []; @@ -96,11 +97,6 @@ public function bgrewriteaof($key_or_address): \RedisCluster|bool return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgrewriteaof(...\func_get_args()); } - public function waitaof($key_or_address, $numlocal, $numreplicas, $timeout): \RedisCluster|array|false - { - return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args()); - } - public function bgsave($key_or_address): \RedisCluster|bool { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgsave(...\func_get_args()); @@ -661,11 +657,6 @@ public function pttl($key): \RedisCluster|false|int return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pttl(...\func_get_args()); } - public function publish($channel, $message): \RedisCluster|bool - { - return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->publish(...\func_get_args()); - } - public function pubsub($key_or_address, ...$values): mixed { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->pubsub(...\func_get_args()); diff --git a/src/Symfony/Component/Cache/Traits/RedisCluster6ProxyTrait.php b/src/Symfony/Component/Cache/Traits/RedisCluster6ProxyTrait.php new file mode 100644 index 0000000000000..389c6e1adf347 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/RedisCluster6ProxyTrait.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +if (version_compare(phpversion('redis'), '6.0.2', '>')) { + /** + * @internal + */ + trait RedisCluster6ProxyTrait + { + public function publish($channel, $message): \RedisCluster|bool|int + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->publish(...\func_get_args()); + } + + public function waitaof($key_or_address, $numlocal, $numreplicas, $timeout): \RedisCluster|array|false + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args()); + } + } +} else { + /** + * @internal + */ + trait RedisCluster6ProxyTrait + { + public function publish($channel, $message): \RedisCluster|bool + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->publish(...\func_get_args()); + } + } +} diff --git a/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php b/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php index 4f720aa97f9f4..e8d7f5cfa237a 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; /** @@ -90,19 +89,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 = '~'; @@ -170,7 +162,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) { @@ -191,7 +183,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); @@ -201,15 +193,18 @@ private function writeArray(array $array, int $depth): void } else { $val = $value; } + $prefix = $asComment ? '# ' : ''; + + $prefix = $asComment ? '# ' : ''; if ($isIndexed) { - $this->writeLine('- '.$val, $depth * 4); + $this->writeLine($prefix.'- '.$val, $depth * 4); } else { - $this->writeLine(sprintf('%-20s %s', $key.':', $val), $depth * 4); + $this->writeLine(sprintf('%s%-20s %s', $prefix, $key.':', $val), $depth * 4); } if (\is_array($value)) { - $this->writeArray($value, $depth + 1); + $this->writeArray($value, $depth + 1, $asComment); } } } diff --git a/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php b/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php index e6ce07588f9d0..84d9f596c1892 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php @@ -109,6 +109,8 @@ enum="" + + EOL diff --git a/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php b/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php index 18ad445c3ef5d..cb33603f6cbb0 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php @@ -114,11 +114,11 @@ enum: ~ # One of "this"; "that"; Symfony\Component\Config\Tests\ # which should be indented child3: ~ # Example: 'example setting' scalar_prototyped: [] - variable: + variable: ~ # Examples: - - foo - - bar + # - foo + # - bar parameters: # Prototype: Parameter name @@ -142,6 +142,11 @@ enum: ~ # One of "this"; "that"; Symfony\Component\Config\Tests\ # Prototype name: [] + array_with_array_example_and_no_default_value: [] + + # Examples: + # - foo + # - bar custom_node: true EOL; diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Configuration/ExampleConfiguration.php b/src/Symfony/Component/Config/Tests/Fixtures/Configuration/ExampleConfiguration.php index bdf6d80bff443..9f62a684a38fa 100644 --- a/src/Symfony/Component/Config/Tests/Fixtures/Configuration/ExampleConfiguration.php +++ b/src/Symfony/Component/Config/Tests/Fixtures/Configuration/ExampleConfiguration.php @@ -97,6 +97,9 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->arrayNode('array_with_array_example_and_no_default_value') + ->example(['foo', 'bar']) + ->end() ->append(new CustomNodeDefinition('acme')) ->end() ; diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index f4eec051c19a7..b406292b44b3a 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -183,9 +183,9 @@ public function setMessage(string $message, string $name = 'message'): void $this->messages[$name] = $message; } - public function getMessage(string $name = 'message'): string + public function getMessage(string $name = 'message'): ?string { - return $this->messages[$name]; + return $this->messages[$name] ?? null; } public function getStartTime(): int diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index bc6697a4cad2b..9f538d639a044 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -367,8 +367,9 @@ public function render(): void if ($headers && !$containsColspan) { if (0 === $idx) { $rows[] = [sprintf( - '%s: %s', - str_pad($headers[$i] ?? '', $maxHeaderLength, ' ', \STR_PAD_LEFT), + '%s%s: %s', + str_repeat(' ', $maxHeaderLength - Helper::width(Helper::removeDecoration($formatter, $headers[$i] ?? ''))), + $headers[$i] ?? '', $part )]; } else { diff --git a/src/Symfony/Component/Console/Tests/Helper/TableTest.php b/src/Symfony/Component/Console/Tests/Helper/TableTest.php index 728ea847f031f..4af12e34c0680 100644 --- a/src/Symfony/Component/Console/Tests/Helper/TableTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/TableTest.php @@ -1649,6 +1649,28 @@ public static function provideRenderVerticalTests(): \Traversable $books, ]; + yield 'With multibyte characters in some headers (the "í" in "Títle") and cells (the "í" in "Dívíne")' => [ + << [ << $tags) { foreach ($tags as $attributes) { $tag = $this->document->createElement('tag'); - if (!\array_key_exists('name', $attributes)) { - $tag->setAttribute('name', $name); - } else { - $tag->appendChild($this->document->createTextNode($name)); - } // Check if we have recursive attributes if (array_filter($attributes, \is_array(...))) { + $tag->setAttribute('name', $name); $this->addTagRecursiveAttributes($tag, $attributes); } else { + if (!\array_key_exists('name', $attributes)) { + $tag->setAttribute('name', $name); + } else { + $tag->appendChild($this->document->createTextNode($name)); + } foreach ($attributes as $key => $value) { $tag->setAttribute($key, $value ?? ''); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 3045367aad1a4..e8db6c987b26c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -34,6 +34,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; @@ -399,6 +400,9 @@ public function testResolveParameter() $this->assertEquals(Foo::class, $container->getDefinition('bar')->getArgument(0)); } + /** + * @group legacy + */ public function testOptionalParameter() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php index f99a3f9eb5196..4a7d87358aad8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php @@ -15,12 +15,12 @@ class Bar implements BarInterface { public $quz; - public function __construct($quz = null, \NonExistent $nonExistent = null, BarInterface $decorated = null, array $foo = [], iterable $baz = []) + public function __construct($quz = null, ?\NonExistent $nonExistent = null, ?BarInterface $decorated = null, array $foo = [], iterable $baz = []) { $this->quz = $quz; } - public static function create(\NonExistent $nonExistent = null, $factory = null) + public static function create(?\NonExistent $nonExistent = null, $factory = null) { } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php index 65437a63ec743..53f8bb7c3221e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php @@ -20,7 +20,7 @@ public function setFoosVariadic(Foo $foo, Foo ...$foos) $this->foo = $foo; } - public function setFoosOptional(Foo $foo, Foo $fooOptional = null) + public function setFoosOptional(Foo $foo, ?Foo $fooOptional = null) { $this->foo = $foo; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php index 4f348895132ca..98ee3a45a6036 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php @@ -6,7 +6,7 @@ class BarOptionalArgument { public $foo; - public function __construct(\stdClass $foo = null) + public function __construct(?\stdClass $foo = null) { $this->foo = $foo; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Foo.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Foo.php index e775def689305..36f027f1dd9c6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Foo.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Foo.php @@ -9,7 +9,7 @@ public static function createBar() return new Bar(new \stdClass()); } - public static function createBarArguments(\stdClass $stdClass, \stdClass $stdClassOptional = null) + public static function createBarArguments(\stdClass $stdClass, ?\stdClass $stdClassOptional = null) { return new Bar($stdClass); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/OptionalParameter.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/OptionalParameter.php new file mode 100644 index 0000000000000..8674c648e9005 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/OptionalParameter.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\Tests\Compiler\A; +use Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface; +use Symfony\Component\DependencyInjection\Tests\Compiler\Foo; + +class OptionalParameter +{ + public function __construct(?CollisionInterface $c = null, A $a, ?Foo $f = null) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php index 807c8b3e20086..0659c968c8e72 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php @@ -8,7 +8,7 @@ #[When(env: 'dev')] class Foo implements FooInterface, Sub\BarInterface { - public function __construct($bar = null, iterable $foo = null, object $baz = null) + public function __construct($bar = null, ?iterable $foo = null, ?object $baz = null) { } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_non_scalar_tags.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_non_scalar_tags.php index 76c69868cc49a..2a1234fa7e26a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_non_scalar_tags.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_non_scalar_tags.php @@ -10,6 +10,7 @@ $container ->register('foo', FooClass::class) ->addTag('foo_tag', [ + 'name' => 'attributeName', 'foo' => 'bar', 'bar' => [ 'foo' => 'bar', diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php index 5f57ef3da0b06..5246587bfa63d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php @@ -16,7 +16,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; @@ -106,7 +106,7 @@ public function __construct(A $a, DInterface $d) class E { - public function __construct(D $d = null) + public function __construct(?D $d = null) { } } @@ -162,13 +162,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) @@ -202,7 +195,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) { } } @@ -226,7 +219,7 @@ public function __construct( */ class ClassForResource { - public function __construct($foo, Bar $bar = null) + public function __construct($foo, ?Bar $bar = null) { } @@ -345,7 +338,7 @@ public function setBar() { } - public function setOptionalNotAutowireable(NotARealClass $n = null) + public function setOptionalNotAutowireable(?NotARealClass $n = null) { } @@ -392,7 +385,7 @@ class DecoratorImpl implements DecoratorInterface class Decorated implements DecoratorInterface { - public function __construct($quz = null, \NonExistent $nonExistent = null, DecoratorInterface $decorated = null, array $foo = []) + public function __construct($quz = null, ?\NonExistent $nonExistent = null, ?DecoratorInterface $decorated = null, array $foo = []) { } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php index 69ca09218812c..0c7cc2a7b7baf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php @@ -107,7 +107,7 @@ public function __construct(string $arg1, #[AutowireDecorated] AsDecoratorInterf #[AsDecorator(decorates: \NonExistent::class, onInvalid: ContainerInterface::NULL_ON_INVALID_REFERENCE)] class AsDecoratorBaz implements AsDecoratorInterface { - public function __construct(#[AutowireDecorated] AsDecoratorInterface $inner = null) + public function __construct(#[AutowireDecorated] ?AsDecoratorInterface $inner = null) { } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php index 846c8fe64797b..6084c42c77dd4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php @@ -83,7 +83,7 @@ public function callPassed() class DummyProxyDumper implements DumperInterface { - public function isProxyCandidate(Definition $definition, bool &$asGhostObject = null, string $id = null): bool + public function isProxyCandidate(Definition $definition, ?bool &$asGhostObject = null, ?string $id = null): bool { $asGhostObject = false; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_array_tags.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_array_tags.xml index 8e910be31431c..ba8d790571e8b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_array_tags.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_array_tags.xml @@ -4,6 +4,7 @@ + attributeName bar bar diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_array_tags.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_array_tags.yml index 3f580df3e30ef..e4f355c045699 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_array_tags.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_array_tags.yml @@ -7,4 +7,4 @@ services: foo: class: Bar\FooClass tags: - - foo_tag: { foo: bar, bar: { foo: bar, bar: foo } } + - foo_tag: { name: attributeName, foo: bar, bar: { foo: bar, bar: foo } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index b065da2455df4..45c4d65b769cd 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -460,7 +460,7 @@ public function testParseServiceTagsWithArrayAttributes() $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); $loader->load('services_with_array_tags.xml'); - $this->assertEquals(['foo_tag' => [['foo' => 'bar', 'bar' => ['foo' => 'bar', 'bar' => 'foo']]]], $container->getDefinition('foo')->getTags()); + $this->assertEquals(['foo_tag' => [['name' => 'attributeName', 'foo' => 'bar', 'bar' => ['foo' => 'bar', 'bar' => 'foo']]]], $container->getDefinition('foo')->getTags()); } public function testParseTagsWithoutNameThrowsException() diff --git a/src/Symfony/Component/ErrorHandler/ErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorHandler.php index c096eaca3886a..d82ce6c7024cc 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorHandler.php @@ -595,6 +595,10 @@ public static function handleFatalError(?array $error = null): void set_exception_handler($h); } if (!$handler) { + if (null === $error && $exitCode = self::$exitCode) { + register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); }); + } + return; } if ($handler !== $h) { @@ -630,8 +634,7 @@ public static function handleFatalError(?array $error = null): void // Ignore this re-throw } - if ($exit && self::$exitCode) { - $exitCode = self::$exitCode; + if ($exit && $exitCode = self::$exitCode) { register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); }); } } diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php index f216a8fba63e1..2e13fc40e71b7 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php @@ -31,6 +31,13 @@ */ class ErrorHandlerTest extends TestCase { + protected function tearDown(): void + { + $r = new \ReflectionProperty(ErrorHandler::class, 'exitCode'); + $r->setAccessible(true); + $r->setValue(null, 0); + } + public function testRegister() { $handler = ErrorHandler::register(); diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/FileLinkFormatterTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/FileLinkFormatterTest.php index a5f6330679ff9..fd6d44e316af1 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/FileLinkFormatterTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/FileLinkFormatterTest.php @@ -27,6 +27,11 @@ public function testWhenNoFileLinkFormatAndNoRequest() public function testAfterUnserialize() { + if (get_cfg_var('xdebug.file_link_format')) { + // There is no way to override "xdebug.file_link_format" option in a test. + $this->markTestSkipped('php.ini has a custom option for "xdebug.file_link_format".'); + } + $ide = $_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? null; $_ENV['SYMFONY_IDE'] = $_SERVER['SYMFONY_IDE'] = null; $sut = unserialize(serialize(new FileLinkFormatter())); diff --git a/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ClassWithAnnotatedParameters.php b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ClassWithAnnotatedParameters.php index 2bac262ddb49d..a9cf0dfcb4d2b 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ClassWithAnnotatedParameters.php +++ b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ClassWithAnnotatedParameters.php @@ -14,14 +14,14 @@ public function fooMethod(string $foo) /** * @param string $bar parameter not implemented yet */ - public function barMethod(/* string $bar = null */) + public function barMethod(/* ?string $bar = null */) { } /** * @param Quz $quz parameter not implemented yet */ - public function quzMethod(/* Quz $quz = null */) + public function quzMethod(/* ?Quz $quz = null */) { } diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index b7fc701182a4b..176535d13c267 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -72,6 +72,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); } @@ -190,7 +193,7 @@ private static function doRemove(array $files, bool $isRecursive): void throw new IOException(sprintf('Failed to remove directory "%s": ', $file).$lastError); } - } elseif (!self::box('unlink', $file) && (str_contains(self::$lastError, 'Permission denied') || file_exists($file))) { + } elseif (!self::box('unlink', $file) && ((self::$lastError && str_contains(self::$lastError, 'Permission denied')) || file_exists($file))) { throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError); } } diff --git a/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php b/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php index cb8ed6a775140..bf4c1466c5894 100644 --- a/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php +++ b/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php @@ -28,7 +28,7 @@ class MockStream * @param string|null $opened_path If the path is opened successfully, and STREAM_USE_PATH is set in options, * opened_path should be set to the full path of the file/resource that was actually opened */ - public function stream_open(string $path, string $mode, int $options, string &$opened_path = null): bool + public function stream_open(string $path, string $mode, int $options, ?string &$opened_path = null): bool { return true; } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php index 77b1e75bd49a5..96bdc7c0de1a1 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php @@ -109,6 +109,10 @@ public function reverseTransform(mixed $value): ?\DateTime throw new TransformationFailedException('Expected a string.'); } + if (str_contains($value, "\0")) { + throw new TransformationFailedException('Null bytes not allowed'); + } + $outputTz = new \DateTimeZone($this->outputTimezone); $dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php index 66ad9ff416e26..f7ef667e769b6 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php @@ -133,6 +133,19 @@ public function testReverseTransformEmpty() $this->assertNull($reverseTransformer->reverseTransform('')); } + public function testReverseTransformWithNullBytes() + { + $transformer = new DateTimeToStringTransformer(); + + $nullByte = \chr(0); + $value = '2024-03-15 21:11:00'.$nullByte; + + $this->expectException(TransformationFailedException::class); + $this->expectExceptionMessage('Null bytes not allowed'); + + $transformer->reverseTransform($value); + } + public function testReverseTransformWithDifferentTimezones() { $reverseTransformer = new DateTimeToStringTransformer('America/New_York', 'Asia/Hong_Kong', 'Y-m-d H:i:s'); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTestCase.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTestCase.php index e86bf9e41ed13..5238e2fd88098 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTestCase.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTestCase.php @@ -40,16 +40,6 @@ public function testPassIdAndNameToView() $this->assertEquals('name', $view->vars['full_name']); } - public function testStripLeadingUnderscoresAndDigitsFromId() - { - $view = $this->factory->createNamed('_09name', $this->getTestedType(), null, $this->getTestOptions()) - ->createView(); - - $this->assertEquals('name', $view->vars['id']); - $this->assertEquals('_09name', $view->vars['name']); - $this->assertEquals('_09name', $view->vars['full_name']); - } - public function testPassIdAndNameToViewWithParent() { $view = $this->factory->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE) diff --git a/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php b/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php index b37bfe5ed2d85..8be0323ae1a9e 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php @@ -19,7 +19,7 @@ class CustomArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable { private array $array; - public function __construct(array $array = null) + public function __construct(?array $array = null) { $this->array = $array ?: []; } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/FixedTranslator.php b/src/Symfony/Component/Form/Tests/Fixtures/FixedTranslator.php index 1fc0fa90165f8..432f2ab12db90 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/FixedTranslator.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/FixedTranslator.php @@ -22,7 +22,7 @@ public function __construct(array $translations) $this->translations = $translations; } - public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string + public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string { return $this->translations[$id] ?? $id; } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/TranslatableTextAlign.php b/src/Symfony/Component/Form/Tests/Fixtures/TranslatableTextAlign.php index 7a5d5cdff68e7..4464088c78103 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/TranslatableTextAlign.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/TranslatableTextAlign.php @@ -20,7 +20,7 @@ enum TranslatableTextAlign implements TranslatableInterface case Center; case Right; - public function trans(TranslatorInterface $translator, string $locale = null): string + public function trans(TranslatorInterface $translator, ?string $locale = null): string { return $translator->trans($this->name, locale: $locale); } diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index e74e0263b985f..4446a031e9695 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -51,6 +51,9 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, private ?LoggerInterface $logger = null; + private int $maxHostConnections; + private int $maxPendingPushes; + /** * An internal object to share state between the client and its responses. */ @@ -69,18 +72,22 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.'); } + $this->maxHostConnections = $maxHostConnections; + $this->maxPendingPushes = $maxPendingPushes; + $this->defaultOptions['buffer'] ??= self::shouldBuffer(...); if ($defaultOptions) { [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions); } - - $this->multi = new CurlClientState($maxHostConnections, $maxPendingPushes); } public function setLogger(LoggerInterface $logger): void { - $this->logger = $this->multi->logger = $logger; + $this->logger = $logger; + if (isset($this->multi)) { + $this->multi->logger = $logger; + } } /** @@ -88,6 +95,8 @@ public function setLogger(LoggerInterface $logger): void */ public function request(string $method, string $url, array $options = []): ResponseInterface { + $multi = $this->ensureState(); + [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions); $scheme = $url['scheme']; $authority = $url['authority']; @@ -165,24 +174,24 @@ public function request(string $method, string $url, array $options = []): Respo } // curl's resolve feature varies by host:port but ours varies by host only, let's handle this with our own DNS map - if (isset($this->multi->dnsCache->hostnames[$host])) { - $options['resolve'] += [$host => $this->multi->dnsCache->hostnames[$host]]; + if (isset($multi->dnsCache->hostnames[$host])) { + $options['resolve'] += [$host => $multi->dnsCache->hostnames[$host]]; } - if ($options['resolve'] || $this->multi->dnsCache->evictions) { + if ($options['resolve'] || $multi->dnsCache->evictions) { // First reset any old DNS cache entries then add the new ones - $resolve = $this->multi->dnsCache->evictions; - $this->multi->dnsCache->evictions = []; + $resolve = $multi->dnsCache->evictions; + $multi->dnsCache->evictions = []; if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) { // DNS cache removals require curl 7.42 or higher - $this->multi->reset(); + $multi->reset(); } foreach ($options['resolve'] as $host => $ip) { $resolve[] = null === $ip ? "-$host:$port" : "$host:$port:$ip"; - $this->multi->dnsCache->hostnames[$host] = $ip; - $this->multi->dnsCache->removals["-$host:$port"] = "-$host:$port"; + $multi->dnsCache->hostnames[$host] = $ip; + $multi->dnsCache->removals["-$host:$port"] = "-$host:$port"; } $curlopts[\CURLOPT_RESOLVE] = $resolve; @@ -285,8 +294,8 @@ public function request(string $method, string $url, array $options = []): Respo $curlopts += $options['extra']['curl']; } - if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) { - unset($this->multi->pushedResponses[$url]); + if ($pushedResponse = $multi->pushedResponses[$url] ?? null) { + unset($multi->pushedResponses[$url]); if (self::acceptPushForRequest($method, $options, $pushedResponse)) { $this->logger?->debug(sprintf('Accepting pushed response: "%s %s"', $method, $url)); @@ -294,7 +303,7 @@ public function request(string $method, string $url, array $options = []): Respo // Reinitialize the pushed response with request's options $ch = $pushedResponse->handle; $pushedResponse = $pushedResponse->response; - $pushedResponse->__construct($this->multi, $url, $options, $this->logger); + $pushedResponse->__construct($multi, $url, $options, $this->logger); } else { $this->logger?->debug(sprintf('Rejecting pushed response: "%s"', $url)); $pushedResponse = null; @@ -304,7 +313,7 @@ public function request(string $method, string $url, array $options = []): Respo if (!$pushedResponse) { $ch = curl_init(); $this->logger?->info(sprintf('Request: "%s %s"', $method, $url)); - $curlopts += [\CURLOPT_SHARE => $this->multi->share]; + $curlopts += [\CURLOPT_SHARE => $multi->share]; } foreach ($curlopts as $opt => $value) { @@ -314,7 +323,7 @@ public function request(string $method, string $url, array $options = []): Respo } } - return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host, $port), CurlClientState::$curlVersion['version_number'], $url); + return $pushedResponse ?? new CurlResponse($multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host, $port), CurlClientState::$curlVersion['version_number'], $url); } public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface @@ -323,9 +332,11 @@ public function stream(ResponseInterface|iterable $responses, ?float $timeout = $responses = [$responses]; } - if ($this->multi->handle instanceof \CurlMultiHandle) { + $multi = $this->ensureState(); + + if ($multi->handle instanceof \CurlMultiHandle) { $active = 0; - while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active)) { + while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($multi->handle, $active)) { } } @@ -334,7 +345,9 @@ public function stream(ResponseInterface|iterable $responses, ?float $timeout = public function reset(): void { - $this->multi->reset(); + if (isset($this->multi)) { + $this->multi->reset(); + } } /** @@ -434,6 +447,16 @@ private static function createRedirectResolver(array $options, string $host, int }; } + private function ensureState(): CurlClientState + { + if (!isset($this->multi)) { + $this->multi = new CurlClientState($this->maxHostConnections, $this->maxPendingPushes); + $this->multi->logger = $this->logger; + } + + return $this->multi; + } + private function findConstantName(int $opt): ?string { $constants = array_filter(get_defined_constants(), static fn ($v, $k) => $v === $opt && 'C' === $k[0] && (str_starts_with($k, 'CURLOPT_') || str_starts_with($k, 'CURLINFO_')), \ARRAY_FILTER_USE_BOTH); diff --git a/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php b/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php index 66372aa8a8149..9372dbe5a0b0d 100644 --- a/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php @@ -21,7 +21,7 @@ class JsonMockResponse extends MockResponse public function __construct(mixed $body = [], array $info = []) { try { - $json = json_encode($body, \JSON_THROW_ON_ERROR); + $json = json_encode($body, \JSON_THROW_ON_ERROR | \JSON_PRESERVE_ZERO_FRACTION); } catch (\JsonException $e) { throw new InvalidArgumentException('JSON encoding failed: '.$e->getMessage(), $e->getCode(), $e); } diff --git a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php index 266b539ef6b59..20cf7ef48291b 100644 --- a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php @@ -58,8 +58,8 @@ public function testHandleIsReinitOnReset() { $httpClient = $this->getHttpClient(__FUNCTION__); - $r = new \ReflectionProperty($httpClient, 'multi'); - $clientState = $r->getValue($httpClient); + $r = new \ReflectionMethod($httpClient, 'ensureState'); + $clientState = $r->invoke($httpClient); $initialShareId = $clientState->share; $httpClient->reset(); self::assertNotSame($initialShareId, $clientState->share); diff --git a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php index a7493100c431d..91ec7ea5f2c7c 100644 --- a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php +++ b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php @@ -24,6 +24,11 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(); + } + public function testItCollectsRequestCount() { $httpClient1 = $this->httpClientThatHasTracedRequests([ diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php index 613d80cb1d3a7..3e81f429622a0 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php @@ -72,10 +72,8 @@ public function testPrepareRequestWithBodyIsArray() public function testNormalizeBodyMultipart() { $file = fopen('php://memory', 'r+'); - stream_context_set_option($file, ['http' => [ - 'filename' => 'test.txt', - 'content_type' => 'text/plain', - ]]); + stream_context_set_option($file, 'http', 'filename', 'test.txt'); + stream_context_set_option($file, 'http', 'content_type', 'text/plain'); fwrite($file, 'foobarbaz'); rewind($file); diff --git a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php index 247588cd359a3..182d52932f724 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php @@ -32,6 +32,11 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(); + } + public function testSendRequest() { $client = new HttplugClient(new NativeHttpClient()); diff --git a/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php b/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php index d4bae3ab5c4a3..d1f4deb03a006 100644 --- a/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php @@ -28,6 +28,11 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(); + } + public function testSendRequest() { $factory = new Psr17Factory(); diff --git a/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php b/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php index b371c08cf4241..768353b04abd1 100644 --- a/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php +++ b/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php @@ -59,6 +59,22 @@ public function testJsonEncodeString() $this->assertSame('application/json', $response->getHeaders()['content-type'][0]); } + public function testJsonEncodeFloat() + { + $client = new MockHttpClient(new JsonMockResponse([ + 'foo' => 1.23, + 'ccc' => 1.0, + 'baz' => 10., + ])); + $response = $client->request('GET', 'https://symfony.com'); + + $this->assertSame([ + 'foo' => 1.23, + 'ccc' => 1., + 'baz' => 10., + ], $response->toArray()); + } + /** * @dataProvider responseHeadersProvider */ diff --git a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php index ba9504ae1c66d..a0e39cc46c851 100644 --- a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php @@ -27,6 +27,11 @@ class RetryableHttpClientTest extends TestCase { + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(); + } + public function testRetryOnError() { $client = new RetryableHttpClient( diff --git a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php index cf437a653bd76..b6a2c03c8f7a3 100644 --- a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php @@ -29,6 +29,11 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(); + } + public function testItTracesRequest() { $httpClient = $this->createMock(HttpClientInterface::class); diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json index 278a9b23601f9..dd34b41baf72e 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -24,7 +24,7 @@ "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/http-client-contracts": "^3", + "symfony/http-client-contracts": "^3.4.1", "symfony/service-contracts": "^2.5|^3" }, "require-dev": { @@ -32,7 +32,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/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 5fa7c07a40ada..617628aa66a3e 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -382,7 +382,7 @@ public static function create(string $uri, string $method = 'GET', array $parame */ public static function setFactory(?callable $callable): void { - self::$requestFactory = $callable; + self::$requestFactory = null === $callable ? null : $callable(...); } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index ad5b29850bbee..68cb6ee43bed5 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -2197,6 +2197,23 @@ public function testFactory() Request::setFactory(null); } + public function testFactoryCallable() + { + $requestFactory = new class { + public function createRequest(): Request + { + return new NewRequest(); + } + }; + + Request::setFactory([$requestFactory, 'createRequest']); + + $this->assertEquals('foo', Request::create('/')->getFoo()); + + Request::setFactory(null); + + } + /** * @dataProvider getLongHostNames */ diff --git a/src/Symfony/Component/HttpKernel/Event/ControllerEvent.php b/src/Symfony/Component/HttpKernel/Event/ControllerEvent.php index aa0999d9e9053..1f8520d29b1ba 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 9ef02f3b8613c..31bbe239be275 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 = '7.0.5'; - public const VERSION_ID = 70005; + public const VERSION = '7.0.6'; + public const VERSION_ID = 70006; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 0; - public const RELEASE_VERSION = 5; + public const RELEASE_VERSION = 6; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2024'; diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/DataCollector/CloneVarDataCollector.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/DataCollector/CloneVarDataCollector.php index e1508bf569b4b..ad011fc49de45 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fixtures/DataCollector/CloneVarDataCollector.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/DataCollector/CloneVarDataCollector.php @@ -25,7 +25,7 @@ public function __construct($varToClone) $this->varToClone = $varToClone; } - public function collect(Request $request, Response $response, \Throwable $exception = null): void + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { $this->data = $this->cloneVar($this->varToClone); } diff --git a/src/Symfony/Component/Lock/Store/PdoStore.php b/src/Symfony/Component/Lock/Store/PdoStore.php index 6c40271c4e6d1..18cd334ce0f4d 100644 --- a/src/Symfony/Component/Lock/Store/PdoStore.php +++ b/src/Symfony/Component/Lock/Store/PdoStore.php @@ -232,10 +232,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 1d8382990c97c..1da6c9f85b31b 100644 --- a/src/Symfony/Component/Lock/Store/RedisStore.php +++ b/src/Symfony/Component/Lock/Store/RedisStore.php @@ -282,7 +282,9 @@ private function getNowCode(): string try { $this->supportTime = 1 === $this->evaluate($script, 'symfony_check_support_time', []); } catch (LockStorageException $e) { - if (!str_contains($e->getMessage(), 'commands not allowed after non deterministic')) { + if (!str_contains($e->getMessage(), 'commands not allowed after non deterministic') + && !str_contains($e->getMessage(), 'is not allowed from script script') + ) { throw $e; } $this->supportTime = false; diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/RemoteEvent/BrevoPayloadConverter.php b/src/Symfony/Component/Mailer/Bridge/Brevo/RemoteEvent/BrevoPayloadConverter.php index 8d556b8f37c9a..5f5f0816c23c6 100644 --- a/src/Symfony/Component/Mailer/Bridge/Brevo/RemoteEvent/BrevoPayloadConverter.php +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/RemoteEvent/BrevoPayloadConverter.php @@ -60,7 +60,10 @@ public function convert(array $payload): AbstractMailerEvent $event->setDate($date); $event->setRecipientEmail($payload['email']); - $event->setTags($payload['tags']); + + if (isset($payload['tags'])) { + $event->setTags($payload['tags']); + } return $event; } diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.json b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.json new file mode 100644 index 0000000000000..4fbeadc58112b --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.json @@ -0,0 +1,13 @@ +{ + "id": 814119, + "email": "example@gmail.com", + "message-id": "<202305311313.92192897094@smtp-relay.mailin.fr>", + "date": "2023-05-31 15:13:08", + "event": "request", + "subject": "Subject Line", + "sending_ip": "127.0.0.1", + "ts_event": 1685538788, + "ts": 1685538788, + "reason": "sent", + "ts_epoch": 1685538788179 +} diff --git a/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.php b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.php new file mode 100644 index 0000000000000..57bb1ff1893ee --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Brevo/Tests/Webhook/Fixtures/request_without_tags.php @@ -0,0 +1,9 @@ +', json_decode(file_get_contents(str_replace('.php', '.json', __FILE__)), true, flags: JSON_THROW_ON_ERROR)); +$wh->setRecipientEmail('example@gmail.com'); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1685538788)); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-failing-sendmail.php b/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-failing-sendmail.php new file mode 100755 index 0000000000000..920b980e0f714 --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-failing-sendmail.php @@ -0,0 +1,4 @@ +#!/usr/bin/env php +assertStringEqualsFile($this->argsPath, __DIR__.'/Fixtures/fake-sendmail.php -ffrom@mail.com recipient@mail.com'); } + + public function testThrowsTransportExceptionOnFailure() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support shebangs nor non-blocking standard streams'); + } + + $mail = new Email(); + $mail + ->from('from@mail.com') + ->to('to@mail.com') + ->subject('Subject') + ->text('Some text') + ; + + $envelope = new DelayedEnvelope($mail); + $envelope->setRecipients([new Address('recipient@mail.com')]); + + $sendmailTransport = new SendmailTransport(self::FAKE_FAILING_SENDMAIL); + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Process failed with exit code 42: Sending failed'); + $sendmailTransport->send($mail, $envelope); + } } diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/DummyStream.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/DummyStream.php index 407c90810b78b..d67671ea10762 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/DummyStream.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/DummyStream.php @@ -77,7 +77,7 @@ public function write(string $bytes, $debug = true): void } elseif (str_starts_with($bytes, 'QUIT')) { $this->nextResponse = '221 Goodbye'; } else { - $this->nextResponse = '250 OK'; + $this->nextResponse = '250 OK queued as 000501c4054c'; } } diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php index 6900320506053..a8d3540a115f9 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php @@ -13,6 +13,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Event\SentMessageEvent; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; @@ -24,6 +26,7 @@ use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\Part\File; use Symfony\Component\Mime\RawMessage; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @group time-sensitive @@ -137,6 +140,37 @@ public function testWriteEncodedRecipientAndSenderAddresses() $this->assertContains("RCPT TO:\r\n", $stream->getCommands()); } + public function testMessageIdFromServerIsEmbeddedInSentMessageEvent() + { + $calls = 0; + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher->expects($this->any()) + ->method('dispatch') + ->with($this->callback(static function ($event) use (&$calls): bool { + ++$calls; + + if (1 === $calls && $event instanceof MessageEvent) { + return true; + } + + if (2 === $calls && $event instanceof SentMessageEvent && '000501c4054c' === $event->getMessage()->getMessageId()) { + return true; + } + + return false; + })); + $transport = new SmtpTransport(new DummyStream(), $eventDispatcher); + + $email = new Email(); + $email->from('sender@example.com'); + $email->to('recipient@example.com'); + $email->text('.'); + + $transport->send($email); + + $this->assertSame(2, $calls); + } + public function testAssertResponseCodeNoCodes() { $this->expectException(LogicException::class); diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php index b4a10634e91ca..2d820c13fe68d 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php @@ -39,7 +39,6 @@ class SmtpTransport extends AbstractTransport private int $pingThreshold = 100; private float $lastMessageTime = 0; private AbstractStream $stream; - private string $mtaResult = ''; private string $domain = '[127.0.0.1]'; public function __construct(?AbstractStream $stream = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) @@ -148,10 +147,6 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess throw $e; } - if ($this->mtaResult && $messageId = $this->parseMessageId($this->mtaResult)) { - $message->setMessageId($messageId); - } - $this->checkRestartThreshold(); return $message; @@ -235,9 +230,13 @@ protected function doSend(SentMessage $message): void $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); throw $e; } - $this->mtaResult = $this->executeCommand("\r\n.\r\n", [250]); + $mtaResult = $this->executeCommand("\r\n.\r\n", [250]); $message->appendDebug($this->stream->getDebug()); $this->lastMessageTime = microtime(true); + + if ($mtaResult && $messageId = $this->parseMessageId($mtaResult)) { + $message->setMessageId($messageId); + } } catch (TransportExceptionInterface $e) { $e->appendDebug($this->stream->getDebug()); $this->lastMessageTime = 0; diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/AbstractStream.php b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/AbstractStream.php index a68748b6c9585..498dc560c3ede 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/AbstractStream.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/AbstractStream.php @@ -30,6 +30,7 @@ abstract class AbstractStream protected $in; /** @var resource|null */ protected $out; + protected $err; private string $debug = ''; @@ -68,7 +69,7 @@ abstract public function initialize(): void; public function terminate(): void { - $this->stream = $this->out = $this->in = null; + $this->stream = $this->err = $this->out = $this->in = null; } public function readLine(): string diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php index 3d59ecfecb7d9..8644d7dad7296 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php @@ -45,14 +45,20 @@ public function initialize(): void } $this->in = &$pipes[0]; $this->out = &$pipes[1]; + $this->err = &$pipes[2]; } public function terminate(): void { if (null !== $this->stream) { fclose($this->in); + $out = stream_get_contents($this->out); fclose($this->out); - proc_close($this->stream); + $err = stream_get_contents($this->err); + fclose($this->err); + if (0 !== $exitCode = proc_close($this->stream)) { + throw new TransportException('Process failed with exit code '.$exitCode.': '.$out.$err); + } } parent::terminate(); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php index a08c102a4b9da..e57dad6cad97e 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php @@ -774,6 +774,73 @@ public function testItCanBeConstructedWithTLSOptionsAndNonTLSDsn() ); } + public function testItCanRetryPublishWhenAMQPConnectionExceptionIsThrown() + { + $factory = new TestAmqpFactory( + $amqpConnection = $this->createMock(\AMQPConnection::class), + $amqpChannel = $this->createMock(\AMQPChannel::class), + $amqpQueue = $this->createMock(\AMQPQueue::class), + $amqpExchange = $this->createMock(\AMQPExchange::class) + ); + + $amqpExchange->expects($this->exactly(2)) + ->method('publish') + ->willReturnOnConsecutiveCalls( + $this->throwException(new \AMQPConnectionException('a socket error occurred')), + null + ); + + $connection = Connection::fromDsn('amqp://localhost', [], $factory); + $connection->publish('body'); + } + + public function testItCanRetryPublishWithDelayWhenAMQPConnectionExceptionIsThrown() + { + $factory = new TestAmqpFactory( + $amqpConnection = $this->createMock(\AMQPConnection::class), + $amqpChannel = $this->createMock(\AMQPChannel::class), + $amqpQueue = $this->createMock(\AMQPQueue::class), + $amqpExchange = $this->createMock(\AMQPExchange::class) + ); + + $amqpExchange->expects($this->exactly(2)) + ->method('publish') + ->willReturnOnConsecutiveCalls( + $this->throwException(new \AMQPConnectionException('a socket error occurred')), + null + ); + + $connection = Connection::fromDsn('amqp://localhost', [], $factory); + $connection->publish('body', [], 5000); + } + + public function testItWillRetryMaxThreeTimesWhenAMQPConnectionExceptionIsThrown() + { + $factory = new TestAmqpFactory( + $amqpConnection = $this->createMock(\AMQPConnection::class), + $amqpChannel = $this->createMock(\AMQPChannel::class), + $amqpQueue = $this->createMock(\AMQPQueue::class), + $amqpExchange = $this->createMock(\AMQPExchange::class) + ); + + $exception = new \AMQPConnectionException('a socket error occurred'); + + $amqpExchange->expects($this->exactly(4)) + ->method('publish') + ->willReturnOnConsecutiveCalls( + $this->throwException($exception), + $this->throwException($exception), + $this->throwException($exception), + $this->throwException($exception), + ); + + self::expectException($exception::class); + self::expectExceptionMessage($exception->getMessage()); + + $connection = Connection::fromDsn('amqp://localhost', [], $factory); + $connection->publish('body'); + } + private function createDelayOrRetryConnection(\AMQPExchange $delayExchange, string $deadLetterExchangeName, string $delayQueueName): Connection { $amqpConnection = $this->createMock(\AMQPConnection::class); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php index b39902623adce..0ae1bff21d7c8 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php @@ -287,19 +287,21 @@ public function publish(string $body, array $headers = [], int $delayInMs = 0, ? $this->setupExchangeAndQueues(); // also setup normal exchange for delayed messages so delay queue can DLX messages to it } - if (0 !== $delayInMs) { - $this->publishWithDelay($body, $headers, $delayInMs, $amqpStamp); + $this->withConnectionExceptionRetry(function () use ($body, $headers, $delayInMs, $amqpStamp) { + if (0 !== $delayInMs) { + $this->publishWithDelay($body, $headers, $delayInMs, $amqpStamp); - return; - } + return; + } - $this->publishOnExchange( - $this->exchange(), - $body, - $this->getRoutingKeyForMessage($amqpStamp), - $headers, - $amqpStamp - ); + $this->publishOnExchange( + $this->exchange(), + $body, + $this->getRoutingKeyForMessage($amqpStamp), + $headers, + $amqpStamp + ); + }); } /** @@ -545,11 +547,16 @@ public function exchange(): \AMQPExchange private function clearWhenDisconnected(): void { if (!$this->channel()->isConnected()) { - unset($this->amqpChannel, $this->amqpExchange, $this->amqpDelayExchange); - $this->amqpQueues = []; + $this->clear(); } } + private function clear(): void + { + unset($this->amqpChannel, $this->amqpExchange, $this->amqpDelayExchange); + $this->amqpQueues = []; + } + private function getDefaultPublishRoutingKey(): ?string { return $this->exchangeOptions['default_publish_routing_key'] ?? null; @@ -566,4 +573,23 @@ private function getRoutingKeyForMessage(?AmqpStamp $amqpStamp): ?string { return $amqpStamp?->getRoutingKey() ?? $this->getDefaultPublishRoutingKey(); } + + private function withConnectionExceptionRetry(callable $callable): void + { + $maxRetries = 3; + $retries = 0; + + retry: + try { + $callable(); + } catch (\AMQPConnectionException $e) { + if (++$retries <= $maxRetries) { + $this->clear(); + + goto retry; + } + + throw $e; + } + } } diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php index c8bc8857017cf..e0b0c8c8b8d6b 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php @@ -154,6 +154,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 29b8d49643d0c..20fb3906d1b13 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -22,7 +22,6 @@ CHANGELOG * Add `HandlerDescriptor::getOptions` * Add support for multiple Redis Sentinel hosts * Add `--all` option to the `messenger:failed:remove` command - * `RejectRedeliveredMessageException` implements `UnrecoverableExceptionInterface` in order to not be retried * Add `WrappedExceptionsInterface` interface for exceptions that hold multiple individual exceptions * Deprecate `HandlerFailedException::getNestedExceptions()`, `HandlerFailedException::getNestedExceptionsOfClass()` and `DelayedMessageHandlingException::getExceptions()` which are replaced by a new `getWrappedExceptions()` method diff --git a/src/Symfony/Component/Messenger/Exception/RejectRedeliveredMessageException.php b/src/Symfony/Component/Messenger/Exception/RejectRedeliveredMessageException.php index 72283878c1b0c..0befccf4a1d1f 100644 --- a/src/Symfony/Component/Messenger/Exception/RejectRedeliveredMessageException.php +++ b/src/Symfony/Component/Messenger/Exception/RejectRedeliveredMessageException.php @@ -14,6 +14,6 @@ /** * @author Tobias Schultze */ -class RejectRedeliveredMessageException extends RuntimeException implements UnrecoverableExceptionInterface +class RejectRedeliveredMessageException extends RuntimeException { } diff --git a/src/Symfony/Component/Messenger/Handler/BatchHandlerInterface.php b/src/Symfony/Component/Messenger/Handler/BatchHandlerInterface.php index a2fce4e1bb1e2..42a8590ee70a8 100644 --- a/src/Symfony/Component/Messenger/Handler/BatchHandlerInterface.php +++ b/src/Symfony/Component/Messenger/Handler/BatchHandlerInterface.php @@ -23,7 +23,7 @@ interface BatchHandlerInterface * @return mixed The number of pending messages in the batch if $ack is not null, * the result from handling the message otherwise */ - // public function __invoke(object $message, Acknowledger $ack = null): mixed; + // public function __invoke(object $message, ?Acknowledger $ack = null): mixed; /** * Flushes any pending buffers. diff --git a/src/Symfony/Component/Mime/Tests/Fixtures/web/index.php b/src/Symfony/Component/Mime/Tests/Fixtures/web/index.php new file mode 100644 index 0000000000000..b3d9bbc7f3711 --- /dev/null +++ b/src/Symfony/Component/Mime/Tests/Fixtures/web/index.php @@ -0,0 +1 @@ +markTestSkipped('"https" stream wrapper is not enabled.'); } - $p = DataPart::fromPath($file = 'https://symfony.com/images/common/logo/logo_symfony_header.png'); + $finder = new PhpExecutableFinder(); + $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', '127.0.0.1:8057'])); + $process->setWorkingDirectory(__DIR__.'/../Fixtures/web'); + $process->start(); + + do { + usleep(50000); + } while (!@fopen('http://127.0.0.1:8057', 'r')); + + $p = DataPart::fromPath($file = 'http://localhost:8057/logo_symfony_header.png'); $content = file_get_contents($file); $this->assertEquals($content, $p->getBody()); $maxLineLength = 76; diff --git a/src/Symfony/Component/Mime/composer.json b/src/Symfony/Component/Mime/composer.json index ad81d11bd0465..2ade1c61d3934 100644 --- a/src/Symfony/Component/Mime/composer.json +++ b/src/Symfony/Component/Mime/composer.json @@ -25,6 +25,7 @@ "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/serializer": "^6.4|^7.0" diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md index 33792585479a4..15840fbd578d9 100644 --- a/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md @@ -21,9 +21,9 @@ CHANGELOG * The bridge is not marked as `@experimental` anymore * [BC BREAK] Change signature of `EsendexTransport::__construct()` method from: - `public function __construct(string $token, string $accountReference, string $from, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)` + `public function __construct(string $token, string $accountReference, string $from, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)` to: - `public function __construct(string $email, string $password, string $accountReference, string $from, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)` + `public function __construct(string $email, string $password, string $accountReference, string $from, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)` 5.2.0 ----- diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md index 4e40ad9fce639..ac0f26f2f01a4 100644 --- a/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md @@ -17,9 +17,9 @@ CHANGELOG * The bridge is not marked as `@experimental` anymore * [BC BREAK] Remove `GoogleChatTransport::setThreadKey()` method, this parameter should now be provided via the constructor, which has changed from: - `__construct(string $space, string $accessKey, string $accessToken, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)` + `__construct(string $space, string $accessKey, string $accessToken, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)` to: - `__construct(string $space, string $accessKey, string $accessToken, string $threadKey = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)` + `__construct(string $space, string $accessKey, string $accessToken, ?string $threadKey = null, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)` * [BC BREAK] Rename the parameter `threadKey` to `thread_key` in DSN 5.2.0 diff --git a/src/Symfony/Component/Notifier/Bridge/Mattermost/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Mattermost/CHANGELOG.md index 8e154d13f0b85..39bc172f8cb7b 100644 --- a/src/Symfony/Component/Notifier/Bridge/Mattermost/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/Mattermost/CHANGELOG.md @@ -6,9 +6,9 @@ CHANGELOG * The bridge is not marked as `@experimental` anymore * [BC BREAK] Change signature of `MattermostTransport::__construct()` method from: - `public function __construct(string $token, string $channel, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, string $path = null)` + `public function __construct(string $token, string $channel, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null, ?string $path = null)` to: - `public function __construct(string $token, string $channel, ?string $path = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)` + `public function __construct(string $token, string $channel, ?string $path = null, ?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null)` 5.1.0 ----- diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 36213a7b172fd..62e4dc5030492 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -401,8 +401,18 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid throw $e; } } elseif (PropertyReadInfo::TYPE_PROPERTY === $type) { - if ($access->canBeReference() && !isset($object->$name) && !\array_key_exists($name, (array) $object) && !(new \ReflectionProperty($class, $name))->hasType()) { - throw new UninitializedPropertyException(sprintf('The property "%s::$%s" is not initialized.', $class, $name)); + if (!isset($object->$name) && !\array_key_exists($name, (array) $object)) { + try { + $r = new \ReflectionProperty($class, $name); + + if ($r->isPublic() && !$r->hasType()) { + throw new UninitializedPropertyException(sprintf('The property "%s::$%s" is not initialized.', $class, $name)); + } + } catch (\ReflectionException $e) { + if (!$ignoreInvalidProperty) { + throw new NoSuchPropertyException(sprintf('Can\'t get a way to read the property "%s" in class "%s".', $property, $class)); + } + } } $result[self::VALUE] = $object->$name; diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php index ca4074ca3229b..ed9944d27a67a 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php @@ -19,7 +19,7 @@ class NonTraversableArrayObject implements \ArrayAccess, \Countable { private $array; - public function __construct(array $array = null) + public function __construct(?array $array = null) { $this->array = $array ?: []; } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php index 50f32960b63b1..070d3b3934cf1 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php @@ -39,4 +39,9 @@ public function __get(string $property) return 'constant value'; } } + + public function __isset(string $property) + { + return \in_array($property, ['magicProperty', 'constantMagicProperty'], true); + } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php index bc7ba3d9ffc4a..2e03358597424 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php @@ -19,7 +19,7 @@ class TraversableArrayObject implements \ArrayAccess, \IteratorAggregate, \Count { private $array; - public function __construct(array $array = null) + public function __construct(?array $array = null) { $this->array = $array ?: []; } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index fc9844944c66e..dc5c5500b18ea 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -990,12 +990,19 @@ public function testGetValuePropertyThrowsExceptionIfUninitializedWithLazyGhost( public function testGetValueGetterThrowsExceptionIfUninitializedWithLazyGhost() { + $lazyGhost = $this->createUninitializedObjectPropertyGhost(); + $this->expectException(UninitializedPropertyException::class); $this->expectExceptionMessage('The property "Symfony\Component\PropertyAccess\Tests\Fixtures\UninitializedObjectProperty::$privateUninitialized" is not readable because it is typed "DateTimeInterface". You should initialize it or declare a default value instead.'); + $this->propertyAccessor->getValue($lazyGhost, 'privateUninitialized'); + } + + public function testIsReadableWithMissingPropertyAndLazyGhost() + { $lazyGhost = $this->createUninitializedObjectPropertyGhost(); - $this->propertyAccessor->getValue($lazyGhost, 'privateUninitialized'); + $this->assertFalse($this->propertyAccessor->isReadable($lazyGhost, 'dummy')); } private function createUninitializedObjectPropertyGhost(): UninitializedObjectProperty diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index bc0bc77342f0e..a66a8dbda4c19 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -274,14 +274,12 @@ public function getReadInfo(string $class, string $property, array $context = [] return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisiblityForMethod($method), $method->isStatic(), false); } - if ($allowMagicGet && $reflClass->hasMethod('__get') && ($reflClass->getMethod('__get')->getModifiers() & $this->methodReflectionFlags)) { - return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, false); + if ($allowMagicGet && $reflClass->hasMethod('__get') && (($r = $reflClass->getMethod('__get'))->getModifiers() & $this->methodReflectionFlags)) { + return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, $r->returnsReference()); } - if ($hasProperty && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { - $reflProperty = $reflClass->getProperty($property); - - return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($reflProperty), $reflProperty->isStatic(), true); + if ($hasProperty && (($r = $reflClass->getProperty($property))->getModifiers() & $this->propertyReflectionFlags)) { + return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($r), $r->isStatic(), true); } if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) { @@ -642,7 +640,7 @@ private function getMutatorMethod(string $class, string $property): ?array continue; } - // Parameter can be optional to allow things like: method(array $foo = null) + // Parameter can be optional to allow things like: method(?array $foo = null) if ($reflectionMethod->getNumberOfParameters() >= 1) { return [$reflectionMethod, $prefix]; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index ecb9f57079a1a..030b0360c06af 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 06fdd9104e60c..d81c44685e5fb 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -44,7 +44,7 @@ protected function setUp(): void /** * @dataProvider typesProvider */ - public function testExtract($property, array $type = null) + public function testExtract($property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); } @@ -75,7 +75,7 @@ public function testInvalid($property) /** * @dataProvider typesWithNoPrefixesProvider */ - public function testExtractTypesWithNoPrefixes($property, array $type = null) + public function testExtractTypesWithNoPrefixes($property, ?array $type = null) { $noPrefixExtractor = new PhpStanExtractor([], [], []); @@ -130,7 +130,7 @@ public static function typesProvider() /** * @dataProvider provideCollectionTypes */ - public function testExtractCollection($property, array $type = null) + public function testExtractCollection($property, ?array $type = null) { $this->testExtract($property, $type); } @@ -186,7 +186,7 @@ public static function provideCollectionTypes() /** * @dataProvider typesWithCustomPrefixesProvider */ - public function testExtractTypesWithCustomPrefixes($property, array $type = null) + public function testExtractTypesWithCustomPrefixes($property, ?array $type = null) { $customExtractor = new PhpStanExtractor(['add', 'remove'], ['is', 'can']); @@ -344,7 +344,7 @@ public static function propertiesParentTypeProvider(): array /** * @dataProvider constructorTypesProvider */ - public function testExtractConstructorTypes($property, array $type = null) + public function testExtractConstructorTypes($property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } @@ -459,7 +459,7 @@ public static function intRangeTypeProvider(): array /** * @dataProvider php80TypesProvider */ - public function testExtractPhp80Type(string $class, $property, array $type = null) + public function testExtractPhp80Type(string $class, $property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes($class, $property, [])); } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index ec3bb8da4e200..c76f7a7c8b296 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -194,7 +194,7 @@ public function getA() * * @param ParentDummy|null $parent */ - public function setB(ParentDummy $parent = null) + public function setB(?ParentDummy $parent = null) { } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures/ExtendedRoute.php b/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures/ExtendedRoute.php index 72232cbf6d50a..dca36a7ea2a56 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures/ExtendedRoute.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures/ExtendedRoute.php @@ -7,7 +7,7 @@ #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class ExtendedRoute extends Route { - public function __construct(array|string $path = null, ?string $name = null, array $defaults = []) + public function __construct(array|string|null $path = null, ?string $name = null, array $defaults = []) { parent::__construct("/{section<(foo|bar|baz)>}" . $path, $name, [], [], array_merge(['section' => 'foo'], $defaults)); } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/RedirectableUrlMatcher.php b/src/Symfony/Component/Routing/Tests/Fixtures/RedirectableUrlMatcher.php index c5f49a83057c3..6c1dd651855ae 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/RedirectableUrlMatcher.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/RedirectableUrlMatcher.php @@ -19,7 +19,7 @@ */ class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface { - public function redirect(string $path, string $route, string $scheme = null): array + public function redirect(string $path, string $route, ?string $scheme = null): array { return [ '_controller' => 'Some controller reference...', diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/TraceableAttributeClassLoader.php b/src/Symfony/Component/Routing/Tests/Fixtures/TraceableAttributeClassLoader.php index 1e2a2637dee8c..36b7619c7df6d 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/TraceableAttributeClassLoader.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/TraceableAttributeClassLoader.php @@ -20,7 +20,7 @@ final class TraceableAttributeClassLoader extends AttributeClassLoader /** @var list */ public array $foundClasses = []; - public function load(mixed $class, string $type = null): RouteCollection + public function load(mixed $class, ?string $type = null): RouteCollection { if (!is_string($class)) { throw new \InvalidArgumentException(sprintf('Expected string, got "%s"', get_debug_type($class))); diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php index 6665c28e8eaf8..7643d9a561923 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php @@ -125,7 +125,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 182ac1ae6e917..083d3dd3f7999 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -78,17 +78,25 @@ protected function extractAttributes(object $object, ?string $format = null, arr if (str_starts_with($name, 'get') || str_starts_with($name, 'has') || str_starts_with($name, 'can')) { // getters, hassers and canners - $attributeName = substr($name, 3); + $attributeName = $name; if (!$reflClass->hasProperty($attributeName)) { - $attributeName = lcfirst($attributeName); + $attributeName = substr($attributeName, 3); + + if (!$reflClass->hasProperty($attributeName)) { + $attributeName = lcfirst($attributeName); + } } } elseif (str_starts_with($name, 'is')) { // issers - $attributeName = substr($name, 2); + $attributeName = $name; if (!$reflClass->hasProperty($attributeName)) { - $attributeName = lcfirst($attributeName); + $attributeName = substr($attributeName, 2); + + if (!$reflClass->hasProperty($attributeName)) { + $attributeName = lcfirst($attributeName); + } } } diff --git a/src/Symfony/Component/Serializer/Tests/Annotation/ContextTest.php b/src/Symfony/Component/Serializer/Tests/Annotation/ContextTest.php index 7efe8dda598d2..9584d6f1b5e51 100644 --- a/src/Symfony/Component/Serializer/Tests/Annotation/ContextTest.php +++ b/src/Symfony/Component/Serializer/Tests/Annotation/ContextTest.php @@ -133,7 +133,7 @@ public static function provideValidInputs(): iterable DUMP ]; - yield 'named arguemnts: with groups option as array' => [ + yield 'named arguments: with groups option as array' => [ fn () => new Context(context: ['foo' => 'bar'], groups: ['a', 'b']), << $this->foo, @@ -33,7 +33,7 @@ public function normalize(NormalizerInterface $normalizer, string $format = null ]; } - public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, string $format = null, array $context = []): void + public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, ?string $format = null, array $context = []): void { $this->foo = $data['foo']; $this->bar = $data['bar']; diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php index 15bcc6e6bec7f..98e0fa06fcd77 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyString.php @@ -22,7 +22,7 @@ class DummyString implements DenormalizableInterface /** @var string $value */ public $value; - public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = []): void + public function denormalize(DenormalizerInterface $denormalizer, $data, ?string $format = null, array $context = []): void { $this->value = $data; } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopeNormalizer.php b/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopeNormalizer.php index 021c22a04c0ef..4acfdf8304743 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopeNormalizer.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopeNormalizer.php @@ -20,7 +20,7 @@ class EnvelopeNormalizer implements NormalizerInterface { private $serializer; - public function normalize($envelope, string $format = null, array $context = []): array + public function normalize($envelope, ?string $format = null, array $context = []): array { $xmlContent = $this->serializer->serialize($envelope->message, 'xml'); @@ -38,7 +38,7 @@ public function getSupportedTypes(?string $format): array ]; } - public function supportsNormalization($data, string $format = null, array $context = []): bool + public function supportsNormalization($data, ?string $format = null, array $context = []): bool { return $data instanceof EnvelopeObject; } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopedMessageNormalizer.php b/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopedMessageNormalizer.php index 5d48f4569cb0a..812dbf015730a 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopedMessageNormalizer.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/EnvelopedMessageNormalizer.php @@ -18,7 +18,7 @@ */ class EnvelopedMessageNormalizer implements NormalizerInterface { - public function normalize($message, string $format = null, array $context = []): array + public function normalize($message, ?string $format = null, array $context = []): array { return [ 'text' => $message->text, @@ -32,7 +32,7 @@ public function getSupportedTypes(?string $format): array ]; } - public function supportsNormalization($data, string $format = null, array $context = []): bool + public function supportsNormalization($data, ?string $format = null, array $context = []): bool { return $data instanceof EnvelopedMessage; } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/FooInterfaceDummyDenormalizer.php b/src/Symfony/Component/Serializer/Tests/Fixtures/FooInterfaceDummyDenormalizer.php index a8c45373b70ee..0fa3c8202ac95 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/FooInterfaceDummyDenormalizer.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/FooInterfaceDummyDenormalizer.php @@ -15,7 +15,7 @@ final class FooInterfaceDummyDenormalizer implements DenormalizerInterface { - public function denormalize(mixed $data, string $type, string $format = null, array $context = []): array + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): array { $result = []; foreach ($data as $foo) { @@ -27,7 +27,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar return $result; } - public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { if (str_ends_with($type, '[]')) { $className = substr($type, 0, -2); diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/NormalizableTraversableDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/NormalizableTraversableDummy.php index d6b38b89af1dc..5165120c3bdb5 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/NormalizableTraversableDummy.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/NormalizableTraversableDummy.php @@ -18,7 +18,7 @@ class NormalizableTraversableDummy extends TraversableDummy implements NormalizableInterface, DenormalizableInterface { - public function normalize(NormalizerInterface $normalizer, string $format = null, array $context = []): array|string|int|float|bool + public function normalize(NormalizerInterface $normalizer, ?string $format = null, array $context = []): array|string|int|float|bool { return [ 'foo' => 'normalizedFoo', @@ -26,7 +26,7 @@ public function normalize(NormalizerInterface $normalizer, string $format = null ]; } - public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, string $format = null, array $context = []): void + public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, ?string $format = null, array $context = []): void { } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php index e8c64f57752dd..41da0eac8a999 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php @@ -24,7 +24,7 @@ public function __construct() { } - public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = []): void + public function denormalize(DenormalizerInterface $denormalizer, $data, ?string $format = null, array $context = []): void { throw new NotNormalizableValueException(); } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodDummy.php new file mode 100644 index 0000000000000..89c8fcb9c399c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodDummy.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +class SamePropertyAsMethodDummy +{ + private $freeTrial; + private $hasSubscribe; + private $getReady; + private $isActive; + + public function __construct($freeTrial, $hasSubscribe, $getReady, $isActive) + { + $this->freeTrial = $freeTrial; + $this->hasSubscribe = $hasSubscribe; + $this->getReady = $getReady; + $this->isActive = $isActive; + } + + public function getFreeTrial() + { + return $this->freeTrial; + } + + public function hasSubscribe() + { + return $this->hasSubscribe; + } + + public function getReady() + { + return $this->getReady; + } + + public function isActive() + { + return $this->isActive; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithMethodSerializedNameDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithMethodSerializedNameDummy.php new file mode 100644 index 0000000000000..203118885853e --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithMethodSerializedNameDummy.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\SerializedName; + +class SamePropertyAsMethodWithMethodSerializedNameDummy +{ + private $freeTrial; + private $hasSubscribe; + private $getReady; + private $isActive; + + public function __construct($freeTrial, $hasSubscribe, $getReady, $isActive) + { + $this->freeTrial = $freeTrial; + $this->hasSubscribe = $hasSubscribe; + $this->getReady = $getReady; + $this->isActive = $isActive; + } + + #[SerializedName('free_trial_method')] + public function getFreeTrial() + { + return $this->freeTrial; + } + + #[SerializedName('has_subscribe_method')] + public function hasSubscribe() + { + return $this->hasSubscribe; + } + + #[SerializedName('get_ready_method')] + public function getReady() + { + return $this->getReady; + } + + #[SerializedName('is_active_method')] + public function isActive() + { + return $this->isActive; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithPropertySerializedNameDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithPropertySerializedNameDummy.php new file mode 100644 index 0000000000000..0b681934f2fab --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/SamePropertyAsMethodWithPropertySerializedNameDummy.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\SerializedName; + +class SamePropertyAsMethodWithPropertySerializedNameDummy +{ + #[SerializedName('free_trial_property')] + private $freeTrial; + + #[SerializedName('has_subscribe_property')] + private $hasSubscribe; + + #[SerializedName('get_ready_property')] + private $getReady; + + #[SerializedName('is_active_property')] + private $isActive; + + public function __construct($freeTrial, $hasSubscribe, $getReady, $isActive) + { + $this->freeTrial = $freeTrial; + $this->hasSubscribe = $hasSubscribe; + $this->getReady = $getReady; + $this->isActive = $isActive; + } + + public function getFreeTrial() + { + return $this->freeTrial; + } + + public function hasSubscribe() + { + return $this->hasSubscribe; + } + + public function getReady() + { + return $this->getReady; + } + + public function isActive() + { + return $this->isActive; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/ScalarDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/ScalarDummy.php index 949407d116b68..50617f33eb5e0 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/ScalarDummy.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/ScalarDummy.php @@ -21,12 +21,12 @@ class ScalarDummy implements NormalizableInterface, DenormalizableInterface public $foo; public $xmlFoo; - public function normalize(NormalizerInterface $normalizer, string $format = null, array $context = []): array|string|int|float|bool + public function normalize(NormalizerInterface $normalizer, ?string $format = null, array $context = []): array|string|int|float|bool { return 'xml' === $format ? $this->xmlFoo : $this->foo; } - public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, string $format = null, array $context = []): void + public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, ?string $format = null, array $context = []): void { if ('xml' === $format) { $this->xmlFoo = $data; diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php b/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php index c2e77aa4baf50..1ba6884bde1d0 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/StaticConstructorNormalizer.php @@ -23,17 +23,17 @@ public function getSupportedTypes(?string $format): array return [StaticConstructorDummy::class]; } - protected function extractAttributes(object $object, string $format = null, array $context = []): array + protected function extractAttributes(object $object, ?string $format = null, array $context = []): array { return get_object_vars($object); } - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed + protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed { return $object->$attribute; } - protected function setAttributeValue(object $object, string $attribute, mixed $value, string $format = null, array $context = []): void + protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void { $object->$attribute = $value; } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index b5719cc453c63..d92f50e22ac84 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -931,12 +931,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}; } @@ -971,12 +971,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}; } @@ -1006,12 +1006,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 0cb0722647697..8285dd88ee22b 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -41,6 +41,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; @@ -835,6 +838,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 @@ -1009,6 +1059,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 ffde49a995964..19b88b68937aa 100644 --- a/src/Symfony/Component/Validator/Constraints/BicValidator.php +++ b/src/Symfony/Component/Validator/Constraints/BicValidator.php @@ -99,16 +99,6 @@ public function validate(mixed $value, Constraint $constraint): void 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 6788df3e1466a..bbdd99c715a2e 100644 --- a/src/Symfony/Component/Validator/Constraints/File.php +++ b/src/Symfony/Component/Validator/Constraints/File.php @@ -38,6 +38,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 f4e4012c642d3..e4e9ddbe5abd0 100644 --- a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php @@ -40,12 +40,12 @@ public function validate(mixed $value, Constraint $constraint): void $collectionElements = []; $normalizer = $this->getNormalizer($constraint); foreach ($value as $element) { + $element = $normalizer($element); + if ($fields && !$element = $this->reduceElementKeys($fields, $element)) { continue; } - $element = $normalizer($element); - if (\in_array($element, $collectionElements, true)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf index 6ac303a778fa9..dfd398ae95a4f 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf @@ -136,7 +136,7 @@ This value is not a valid IP address. - هذه القيمة ليست عنوان IP صالحًا. + هذا ليس عنوان IP صحيح. This value is not a valid language. @@ -192,7 +192,7 @@ No temporary folder was configured in php.ini, or the configured folder does not exist. - لم يتم تكوين مجلد مؤقت في ملف php.ini، أو المجلد المعد لا يوجد. + لم يتم تكوين مجلد مؤقت في ملف php.ini. Cannot write temporary file to disk. @@ -224,7 +224,7 @@ This value is not a valid International Bank Account Number (IBAN). - هذه القيمة ليست رقم حساب بنكي دولي (IBAN) صالحًا. + هذه القيمة ليست رقم حساب بنكي دولي (IBAN) صالحًا. This value is not a valid ISBN-10. @@ -312,7 +312,7 @@ This value is not a valid Business Identifier Code (BIC). - هذه القيمة ليست رمز معرف الأعمال (BIC) صالحًا. + هذه القيمة ليست رمز معرف أعمال (BIC) صالحًا. Error @@ -320,7 +320,7 @@ This value is not a valid UUID. - هذه القيمة ليست UUID صالحًا. + هذه القيمة ليست UUID صالحًا. This value should be a multiple of {{ compared_value }}. @@ -432,11 +432,11 @@ The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}. - تم اكتشاف ترميز الأحرف غير صالح ({{ detected }}). الترميزات المسموح بها هي {{ encodings }}. + تم اكتشاف ترميز أحرف غير صالح ({{ detected }}). الترميزات المسموح بها هي {{ encodings }}. This value is not a valid MAC address. - هذه القيمة ليست عنوان MAC صالحًا. + هذه القيمة ليست عنوان MAC صالحًا. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf index f7677fabfb89a..db927e1d51e65 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.el.xlf @@ -136,7 +136,7 @@ This value is not a valid IP address. - Αυτή η τιμή δεν είναι έγκυρη διεύθυνση IP. + Αυτή η IP διεύθυνση δεν είναι έγκυρη. This value is not a valid language. @@ -192,7 +192,7 @@ No temporary folder was configured in php.ini, or the configured folder does not exist. - Δεν ρυθμίστηκε προσωρινός φάκελος στο php.ini, ή ο ρυθμισμένος φάκελος δεν υπάρχει. + Δεν έχει ρυθμιστεί προσωρινός φάκελος στο php.ini, ή ο ρυθμισμένος φάκελος δεν υπάρχει. Cannot write temporary file to disk. @@ -224,7 +224,7 @@ This value is not a valid International Bank Account Number (IBAN). - Αυτή η τιμή δεν είναι έγκυρος Διεθνής Αριθμός Τραπεζικού Λογαριασμού (IBAN). + Αυτός δεν είναι έγκυρος διεθνής αριθμός τραπεζικού λογαριασμού (IBAN). This value is not a valid ISBN-10. @@ -312,7 +312,7 @@ This value is not a valid Business Identifier Code (BIC). - Αυτή η τιμή δεν είναι έγκυρος Κωδικός Ταυτοποίησης Επιχείρησης (BIC). + Αυτός ο αριθμός δεν είναι έγκυρος Κωδικός Ταυτοποίησης Επιχείρησης (BIC). Error @@ -320,7 +320,7 @@ This value is not a valid UUID. - Αυτή η τιμή δεν είναι έγκυρη UUID. + Αυτός ο αριθμός δεν είναι έγκυρη UUID. This value should be a multiple of {{ compared_value }}. @@ -436,7 +436,7 @@ This value is not a valid MAC address. - Αυτή η τιμή δεν είναι έγκυρη διεύθυνση MAC. + Αυτός ο αριθμός δεν είναι έγκυρη διεύθυνση MAC. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php index 699979f3e6f63..8b3c815423a48 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php @@ -190,7 +190,6 @@ public function testValidBics($bic) public static function getValidBics() { - // http://formvalidation.io/validators/bic/ return [ ['ASPKAT2LXXX'], ['ASPKAT2L'], @@ -198,6 +197,7 @@ public static function getValidBics() ['UNCRIT2B912'], ['DABADKKK'], ['RZOOAT2L303'], + ['1SBACNBXSHA'], ]; } @@ -241,11 +241,6 @@ public static function getInvalidBics() ['ASPKAT2LX', Bic::INVALID_LENGTH_ERROR], ['ASPKAT2LXXX1', Bic::INVALID_LENGTH_ERROR], ['DABADKK', Bic::INVALID_LENGTH_ERROR], - ['1SBACNBXSHA', Bic::INVALID_BANK_CODE_ERROR], - ['RZ00AT2L303', Bic::INVALID_BANK_CODE_ERROR], - ['D2BACNBXSHA', Bic::INVALID_BANK_CODE_ERROR], - ['DS3ACNBXSHA', Bic::INVALID_BANK_CODE_ERROR], - ['DSB4CNBXSHA', Bic::INVALID_BANK_CODE_ERROR], ['DEUT12HH', Bic::INVALID_COUNTRY_CODE_ERROR], ['DSBAC6BXSHA', Bic::INVALID_COUNTRY_CODE_ERROR], ['DSBA5NBXSHA', Bic::INVALID_COUNTRY_CODE_ERROR], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php index edacf3312c999..d15e41660b7d3 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php @@ -60,14 +60,17 @@ public function testSuspiciousStrings(string $string, array $options, array $err { $this->validator->validate($string, new NoSuspiciousCharacters($options)); - $violations = $this->buildViolation(reset($errors)) - ->setCode(key($errors)) - ->setParameter('{{ value }}', '"'.$string.'"') - ; - - while ($message = next($errors)) { - $violations = $violations->buildNextViolation($message) - ->setCode(key($errors)) + $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.'"') ; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php index 35bde67d9bd3f..3c2dd9f21c98f 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Dummy\DummyClassOne; class UniqueValidatorTest extends ConstraintValidatorTestCase { @@ -283,6 +284,36 @@ public static function getInvalidCollectionValues(): array ], ]; } + + public function testArrayOfObjectsUnique() + { + $array = [ + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + ]; + + $array[0]->code = '1'; + $array[1]->code = '2'; + $array[2]->code = '3'; + + $this->validator->validate( + $array, + new Unique( + normalizer: [self::class, 'normalizeDummyClassOne'], + fields: 'code' + ) + ); + + $this->assertNoViolation(); + } + + public static function normalizeDummyClassOne(DummyClassOne $obj): array + { + return [ + 'code' => $obj->code, + ]; + } } class CallableClass diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithRequiredArgument.php b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithRequiredArgument.php index 3048ae5ce1b99..f8abc8a563f52 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithRequiredArgument.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithRequiredArgument.php @@ -20,7 +20,7 @@ final class ConstraintWithRequiredArgument extends Constraint public string $requiredArg; #[HasNamedArguments] - public function __construct(string $requiredArg, array $groups = null, mixed $payload = null) + public function __construct(string $requiredArg, ?array $groups = null, mixed $payload = null) { parent::__construct([], $groups, $payload); diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php b/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php index ee5b9c22db3e9..dab811dc2182d 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php @@ -19,7 +19,7 @@ class CustomArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable { private $array; - public function __construct(array $array = null) + public function __construct(?array $array = null) { $this->array = $array ?: []; } diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php index f42d06cb642ef..137917b63e95c 100644 --- a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php @@ -116,10 +116,16 @@ public static function castType(\ReflectionType $c, array $a, Stub $stub, bool $ public static function castAttribute(\ReflectionAttribute $c, array $a, Stub $stub, bool $isNested): array { - self::addMap($a, $c, [ + $map = [ 'name' => 'getName', 'arguments' => 'getArguments', - ]); + ]; + + if (\PHP_VERSION_ID >= 80400) { + unset($map['name']); + } + + self::addMap($a, $c, $map); return $a; } @@ -362,7 +368,7 @@ public static function getSignature(array $a): string 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/Cloner/Stub.php b/src/Symfony/Component/VarDumper/Cloner/Stub.php index 4070b981d2374..4455a362b88cb 100644 --- a/src/Symfony/Component/VarDumper/Cloner/Stub.php +++ b/src/Symfony/Component/VarDumper/Cloner/Stub.php @@ -35,7 +35,7 @@ class Stub public int $type = self::TYPE_REF; public string|int|null $class = ''; - public mixed $value; + public mixed $value = null; public int $cut = 0; public int $handle = 0; public int $refCount = 0; diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index 5581144cd2f76..9fe1a9b664ab1 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -531,13 +531,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: [] } ] @@ -550,14 +551,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" ] @@ -572,14 +574,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" @@ -594,8 +597,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" ] @@ -623,14 +627,15 @@ public function testReflectionClassConstantWithAttribute() public function testReflectionParameterWithAttribute() { $var = new \ReflectionParameter([LotsOfAttributes::class, 'someMethod'], 'someParameter'); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" arguments: array:1 [ 0 => "three" ] diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php index 8b3e12b5c1d72..cf0bc7338326d 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php @@ -155,7 +155,7 @@ public function testClassStub() $expectedDump = <<<'EODUMP' array:1 [ - 0 => "hello(?stdClass $a, stdClass $b = null)" + 0 => "hello(?stdClass $a, ?stdClass $b = null)" ] EODUMP; diff --git a/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php b/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php index 1e0e6bda05751..824c9f10df606 100644 --- a/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php @@ -90,7 +90,7 @@ public function testGet() +foo: ""…3 +"bar": "bar" } - "closure" => Closure(\$a, PDO &\$b = null) {#%d + "closure" => Closure(\$a, ?PDO &\$b = null) {#%d class: "Symfony\Component\VarDumper\Tests\Dumper\CliDumperTest" this: Symfony\Component\VarDumper\Tests\Dumper\CliDumperTest {#%d …} file: "%s%eTests%eFixtures%edumb-var.php" diff --git a/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php b/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php index c31d07d2f26a5..9b914ad6d3c37 100644 --- a/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php @@ -84,7 +84,7 @@ public function testGet() +foo: "foo" +"bar": "bar" } - "closure" => Closure(\$a, PDO &\$b = null) {#%d + "closure" => Closure(\$a, ?PDO &\$b = null) {#%d class: "Symfony\Component\VarDumper\Tests\Dumper\HtmlDumperTest" this: =8.2" }, "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0" }, "autoload": { diff --git a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php index 98838ef51ef98..ec87d06d1f883 100644 --- a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php +++ b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php @@ -28,6 +28,12 @@ public static function setUpBeforeClass(): void TestHttpServer::start(); } + public static function tearDownAfterClass(): void + { + TestHttpServer::stop(8067); + TestHttpServer::stop(8077); + } + abstract protected function getHttpClient(string $testCase): HttpClientInterface; public function testGetRequest() diff --git a/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php b/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php index 86dfa7de90092..2a278479c2e64 100644 --- a/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php +++ b/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php @@ -45,4 +45,11 @@ public static function start(int $port = 8057/* , string $workingDirectory = nul return $process; } + + public static function stop(int $port = 8057) + { + if (isset(self::$process[$port])) { + self::$process[$port]->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