diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5f2d77a453eaf..d4dafb2aa0029 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 7.3 for features / 6.4, and 7.2 for bug fixes +| Branch? | 7.4 for features / 6.4, 7.2, or 7.3 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index a82202d055cc9..1033e761a2d0b 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -6,7 +6,7 @@ on: schedule: - cron: '34 4 * * 6' push: - branches: [ "7.3" ] + branches: [ "7.4" ] # Declare default permissions as read only. permissions: read-all diff --git a/CHANGELOG-7.3.md b/CHANGELOG-7.3.md index 91adc8732cd35..bee0295a98485 100644 --- a/CHANGELOG-7.3.md +++ b/CHANGELOG-7.3.md @@ -7,6 +7,12 @@ in 7.3 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v7.3.0...v7.3.1 +* 7.3.0 (2025-05-29) + + * bug #60549 [Translation] Add intl-icu fallback for MessageCatalogue metadata (pontus-mp) + * bug #60571 [ErrorHandler] Do not transform file to link if it does not exist (lyrixx) + * bug #60542 [Webhook] Fix controller service name (HypeMC) + * 7.3.0-RC1 (2025-05-25) * bug #60529 [AssetMapper] Fix SequenceParser possible infinite loop (smnandre) diff --git a/UPGRADE-7.3.md b/UPGRADE-7.3.md index 77a3f14c3445b..5c279372b7626 100644 --- a/UPGRADE-7.3.md +++ b/UPGRADE-7.3.md @@ -179,9 +179,7 @@ Security ```php protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool { - $vote ??= new Vote(); - - $vote->reasons[] = 'A brief explanation of why access is granted or denied, as appropriate.'; + $vote?->addReason('A brief explanation of why access is granted or denied, as appropriate.'); } ``` diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/webhook.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/webhook.php index ea80311599fa0..177606b26214e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/webhook.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/webhook.php @@ -24,7 +24,7 @@ } $routes->add('_webhook_controller', '/{type}') - ->controller('webhook_controller::handle') + ->controller('webhook.controller::handle') ->requirements(['type' => '.+']) ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php index b8e7530bb3e01..fd4a008341cb4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php @@ -5,6 +5,7 @@ 'http_method_override' => false, 'handle_all_throwables' => true, 'php_errors' => ['log' => true], + 'lock' => false, 'messenger' => [ 'default_bus' => 'messenger.bus.commands', 'buses' => [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml index dcf402e1a36ec..3f0d96249959e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml @@ -8,6 +8,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml index f06d534a55ec2..38fca57379fcb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml @@ -4,6 +4,7 @@ framework: handle_all_throwables: true php_errors: log: true + lock: false messenger: default_bus: messenger.bus.commands buses: diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php index d0d1246a1ddbd..7bd9a083e53a4 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php @@ -233,6 +233,10 @@ private function formatFile(string $file, int $line, ?string $text = null): stri $text .= ' at line '.$line; } + if (!file_exists($file)) { + return $text; + } + $link = $this->fileLinkFormat->format($file, $line); return \sprintf('%s', $this->escape($link), $text); diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 566e721bf3bb3..bfef40fac58ad 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -73,12 +73,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.3.0-RC1'; + public const VERSION = '7.3.0'; public const VERSION_ID = 70300; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 3; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'RC1'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '05/2025'; public const END_OF_LIFE = '01/2026'; diff --git a/src/Symfony/Component/ObjectMapper/composer.json b/src/Symfony/Component/ObjectMapper/composer.json index eb89582d8aad6..6d1b445d92781 100644 --- a/src/Symfony/Component/ObjectMapper/composer.json +++ b/src/Symfony/Component/ObjectMapper/composer.json @@ -32,5 +32,6 @@ }, "conflict": { "symfony/property-access": "<7.2" - } + }, + "minimum-stability": "dev" } diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php index 1403aaaaf0b15..3ab6b92c1d956 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php @@ -45,11 +45,10 @@ public function __construct( */ public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int { - $vote = 3 < \func_num_args() ? func_get_arg(3) : new Vote(); - $vote ??= new Vote(); + $vote = 3 < \func_num_args() ? func_get_arg(3) : null; if ($attributes === [self::PUBLIC_ACCESS]) { - $vote->reasons[] = 'Access is public.'; + $vote?->addReason('Access is public.'); return VoterInterface::ACCESS_GRANTED; } @@ -73,7 +72,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/* if ((self::IS_AUTHENTICATED_FULLY === $attribute || self::IS_AUTHENTICATED_REMEMBERED === $attribute) && $this->authenticationTrustResolver->isFullFledged($token) ) { - $vote->reasons[] = 'The user is fully authenticated.'; + $vote?->addReason('The user is fully authenticated.'); return VoterInterface::ACCESS_GRANTED; } @@ -81,32 +80,32 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/* if (self::IS_AUTHENTICATED_REMEMBERED === $attribute && $this->authenticationTrustResolver->isRememberMe($token) ) { - $vote->reasons[] = 'The user is remembered.'; + $vote?->addReason('The user is remembered.'); return VoterInterface::ACCESS_GRANTED; } if (self::IS_AUTHENTICATED === $attribute && $this->authenticationTrustResolver->isAuthenticated($token)) { - $vote->reasons[] = 'The user is authenticated.'; + $vote?->addReason('The user is authenticated.'); return VoterInterface::ACCESS_GRANTED; } if (self::IS_REMEMBERED === $attribute && $this->authenticationTrustResolver->isRememberMe($token)) { - $vote->reasons[] = 'The user is remembered.'; + $vote?->addReason('The user is remembered.'); return VoterInterface::ACCESS_GRANTED; } if (self::IS_IMPERSONATOR === $attribute && $token instanceof SwitchUserToken) { - $vote->reasons[] = 'The user is impersonating another user.'; + $vote?->addReason('The user is impersonating another user.'); return VoterInterface::ACCESS_GRANTED; } } if (VoterInterface::ACCESS_DENIED === $result) { - $vote->reasons[] = 'The user is not appropriately authenticated.'; + $vote?->addReason('The user is not appropriately authenticated.'); } return $result; diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/ClosureVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/ClosureVoter.php index 03a9f7571a571..4fb5502fd91c5 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/ClosureVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/ClosureVoter.php @@ -42,7 +42,6 @@ public function supportsType(string $subjectType): bool public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int { - $vote ??= new Vote(); $context = new IsGrantedContext($token, $token->getUser(), $this->authorizationChecker); $failingClosures = []; $result = VoterInterface::ACCESS_ABSTAIN; @@ -54,7 +53,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes, ? $name = (new \ReflectionFunction($attribute))->name; $result = VoterInterface::ACCESS_DENIED; if ($attribute($context, $subject)) { - $vote->reasons[] = \sprintf('Closure %s returned true.', $name); + $vote?->addReason(\sprintf('Closure %s returned true.', $name)); return VoterInterface::ACCESS_GRANTED; } @@ -63,7 +62,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes, ? } if ($failingClosures) { - $vote->reasons[] = \sprintf('Closure%s %s returned false.', 1 < \count($failingClosures) ? 's' : '', implode(', ', $failingClosures)); + $vote?->addReason(\sprintf('Closure%s %s returned false.', 1 < \count($failingClosures) ? 's' : '', implode(', ', $failingClosures))); } return $result; diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php index 35d727a8eb15e..719aae7d46872 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php @@ -49,8 +49,7 @@ public function supportsType(string $subjectType): bool */ public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int { - $vote = 3 < \func_num_args() ? func_get_arg(3) : new Vote(); - $vote ??= new Vote(); + $vote = 3 < \func_num_args() ? func_get_arg(3) : null; $result = VoterInterface::ACCESS_ABSTAIN; $variables = null; $failingExpressions = []; @@ -64,7 +63,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/* $result = VoterInterface::ACCESS_DENIED; if ($this->expressionLanguage->evaluate($attribute, $variables)) { - $vote->reasons[] = \sprintf('Expression (%s) is true.', $attribute); + $vote?->addReason(\sprintf('Expression (%s) is true.', $attribute)); return VoterInterface::ACCESS_GRANTED; } @@ -73,7 +72,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/* } if ($failingExpressions) { - $vote->reasons[] = \sprintf('Expression (%s) is false.', implode(') || (', $failingExpressions)); + $vote?->addReason(\sprintf('Expression (%s) is false.', implode(') || (', $failingExpressions))); } return $result; diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php index 46c08d15b48ed..2225e8d4d4c41 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php @@ -30,8 +30,7 @@ public function __construct( */ public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int { - $vote = 3 < \func_num_args() ? func_get_arg(3) : new Vote(); - $vote ??= new Vote(); + $vote = 3 < \func_num_args() ? func_get_arg(3) : null; $result = VoterInterface::ACCESS_ABSTAIN; $roles = $this->extractRoles($token); $missingRoles = []; @@ -44,7 +43,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/* $result = VoterInterface::ACCESS_DENIED; if (\in_array($attribute, $roles, true)) { - $vote->reasons[] = \sprintf('The user has %s.', $attribute); + $vote?->addReason(\sprintf('The user has %s.', $attribute)); return VoterInterface::ACCESS_GRANTED; } @@ -53,7 +52,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/* } if (VoterInterface::ACCESS_DENIED === $result) { - $vote->reasons[] = \sprintf('The user doesn\'t have%s %s.', 1 < \count($missingRoles) ? ' any of' : '', implode(', ', $missingRoles)); + $vote?->addReason(\sprintf('The user doesn\'t have%s %s.', 1 < \count($missingRoles) ? ' any of' : '', implode(', ', $missingRoles))); } return $result; diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php index 47572797ee906..ec92606359859 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php @@ -32,9 +32,9 @@ public function __construct( public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int { - $result = $this->voter->vote($token, $subject, $attributes, $vote ??= new Vote()); + $result = $this->voter->vote($token, $subject, $attributes, $vote); - $this->eventDispatcher->dispatch(new VoteEvent($this->voter, $subject, $attributes, $result, $vote->reasons), 'debug.security.authorization.vote'); + $this->eventDispatcher->dispatch(new VoteEvent($this->voter, $subject, $attributes, $result, $vote->reasons ?? []), 'debug.security.authorization.vote'); return $result; } diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php index 3d7fd9e2d7a1f..55930def8fda9 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php @@ -29,10 +29,9 @@ abstract class Voter implements VoterInterface, CacheableVoterInterface */ public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int { - $vote = 3 < \func_num_args() ? func_get_arg(3) : new Vote(); - $vote ??= new Vote(); + $vote = 3 < \func_num_args() ? func_get_arg(3) : null; // abstain vote by default in case none of the attributes are supported - $vote->result = self::ACCESS_ABSTAIN; + $voteResult = self::ACCESS_ABSTAIN; foreach ($attributes as $attribute) { try { @@ -48,15 +47,27 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes/* } // as soon as at least one attribute is supported, default is to deny access - $vote->result = self::ACCESS_DENIED; + $voteResult = self::ACCESS_DENIED; + + if (null !== $vote) { + $vote->result = $voteResult; + } if ($this->voteOnAttribute($attribute, $subject, $token, $vote)) { // grant access as soon as at least one attribute returns a positive response - return $vote->result = self::ACCESS_GRANTED; + if (null !== $vote) { + $vote->result = self::ACCESS_GRANTED; + } + + return self::ACCESS_GRANTED; } } - return $vote->result; + if (null !== $vote) { + $vote->result = $voteResult; + } + + return $voteResult; } /** diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php index a8f87e09da7e6..eaada3061dbfe 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php @@ -33,35 +33,51 @@ public static function getTests(): array return [ [$voter, ['EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access'], + [$voter, ['EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access', new Vote()], [$voter, ['CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if attribute and class are supported and attribute does not grant access'], + [$voter, ['CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if attribute and class are supported and attribute does not grant access', new Vote()], [$voter, ['DELETE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute is supported and grants access'], + [$voter, ['DELETE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute is supported and grants access', new Vote()], [$voter, ['DELETE', 'CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if one attribute is supported and denies access'], + [$voter, ['DELETE', 'CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if one attribute is supported and denies access', new Vote()], [$voter, ['CREATE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute grants access'], + [$voter, ['CREATE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute grants access', new Vote()], [$voter, ['DELETE'], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attribute is supported'], + [$voter, ['DELETE'], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attribute is supported', new Vote()], [$voter, ['EDIT'], VoterInterface::ACCESS_ABSTAIN, new class {}, 'ACCESS_ABSTAIN if class is not supported'], + [$voter, ['EDIT'], VoterInterface::ACCESS_ABSTAIN, new class {}, 'ACCESS_ABSTAIN if class is not supported', new Vote()], [$voter, ['EDIT'], VoterInterface::ACCESS_ABSTAIN, null, 'ACCESS_ABSTAIN if object is null'], + [$voter, ['EDIT'], VoterInterface::ACCESS_ABSTAIN, null, 'ACCESS_ABSTAIN if object is null', new Vote()], [$voter, [], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attributes were provided'], + [$voter, [], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attributes were provided', new Vote()], [$voter, [new StringableAttribute()], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access'], + [$voter, [new StringableAttribute()], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access', new Vote()], [$voter, [new \stdClass()], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if attributes were not strings'], + [$voter, [new \stdClass()], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if attributes were not strings', new Vote()], [$integerVoter, [42], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute is an integer'], + [$integerVoter, [42], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute is an integer', new Vote()], ]; } /** * @dataProvider getTests */ - public function testVote(VoterInterface $voter, array $attributes, $expectedVote, $object, $message) + public function testVote(VoterInterface $voter, array $attributes, $expectedVote, $object, $message, ?Vote $vote = null) { - $this->assertEquals($expectedVote, $voter->vote($this->token, $object, $attributes), $message); + $this->assertSame($expectedVote, $voter->vote($this->token, $object, $attributes, $vote), $message); + + if (null !== $vote) { + self::assertSame($expectedVote, $vote->result); + } } public function testVoteWithTypeError() diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php index 73494f405468c..d34b31f2bdeb8 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php @@ -232,7 +232,7 @@ protected function supports(string $attribute, mixed $subject): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool { - $vote->reasons[] = 'Because I can 😈.'; + $vote?->addReason('Because I can 😈.'); return false; } diff --git a/src/Symfony/Component/Translation/MessageCatalogue.php b/src/Symfony/Component/Translation/MessageCatalogue.php index 2d229f2dd1839..eac50bb1f9b9e 100644 --- a/src/Symfony/Component/Translation/MessageCatalogue.php +++ b/src/Symfony/Component/Translation/MessageCatalogue.php @@ -217,6 +217,16 @@ public function getMetadata(string $key = '', string $domain = 'messages'): mixe return $this->metadata; } + if (isset($this->metadata[$domain.self::INTL_DOMAIN_SUFFIX])) { + if ('' === $key) { + return $this->metadata[$domain.self::INTL_DOMAIN_SUFFIX]; + } + + if (isset($this->metadata[$domain.self::INTL_DOMAIN_SUFFIX][$key])) { + return $this->metadata[$domain.self::INTL_DOMAIN_SUFFIX][$key]; + } + } + if (isset($this->metadata[$domain])) { if ('' == $key) { return $this->metadata[$domain]; diff --git a/src/Symfony/Component/Translation/Tests/Catalogue/MessageCatalogueTest.php b/src/Symfony/Component/Translation/Tests/Catalogue/MessageCatalogueTest.php new file mode 100644 index 0000000000000..1ac61673999b2 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Catalogue/MessageCatalogueTest.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\Component\Translation\Tests\Catalogue; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Translation\MessageCatalogue; + +class MessageCatalogueTest extends TestCase +{ + public function testIcuMetadataKept() + { + $mc = new MessageCatalogue('en', ['messages' => ['a' => 'new_a']]); + $metadata = ['metadata' => 'value']; + $mc->setMetadata('a', $metadata, 'messages+intl-icu'); + $this->assertEquals($metadata, $mc->getMetadata('a', 'messages')); + $this->assertEquals($metadata, $mc->getMetadata('a', 'messages+intl-icu')); + } +} diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf index 0e0de772720c4..1781b1f29ec64 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf @@ -468,7 +468,7 @@ This value is not a valid Twig template. - Deze waarde is geen geldige Twig-template. + Deze waarde is geen geldige Twig-template.