diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 0e8c7cc123143..5b77bf4d6539b 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -33,6 +33,7 @@ jobs: mode: low-deps - php: '8.3' - php: '8.4' + - php: '8.5' #mode: experimental fail-fast: false diff --git a/CHANGELOG-7.2.md b/CHANGELOG-7.2.md index 0e61bef1eaa74..11ad8ff6b9959 100644 --- a/CHANGELOG-7.2.md +++ b/CHANGELOG-7.2.md @@ -7,6 +7,18 @@ in 7.2 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.2.0...v7.2.1 +* 7.2.1 (2024-12-11) + + * bug #59145 [TypeInfo] Make `Type::nullable` method no-op on every nullable type (mtarld) + * bug #59122 [Notifier] fix desktop channel bus abstract arg (raphael-geffroy) + * bug #59124 [FrameworkBundle] fix: notifier push channel bus abstract arg (raphael-geffroy) + * bug #59069 [Console] Fix division by 0 error (Rindula) + * bug #59086 [FrameworkBundle] Make uri_signer lazy and improve error when kernel.secret is empty (nicolas-grekas) + * bug #59099 [sendgrid-mailer] Fix null check on region (AUDUL) + * bug #59070 [PropertyInfo] evaluate access flags for properties with asymmetric visibility (xabbuh) + * bug #59062 [HttpClient] Always set CURLOPT_CUSTOMREQUEST to the correct HTTP method in CurlHttpClient (KurtThiemann) + * bug #59059 [TwigBridge] generate conflict-free variable names (xabbuh) + * 7.2.0 (2024-11-29) * bug #59023 [HttpClient] Fix streaming and redirecting with NoPrivateNetworkHttpClient (nicolas-grekas) diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/AssociatedEntityDto.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/AssociatedEntityDto.php new file mode 100644 index 0000000000000..d6f82f8214846 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/AssociatedEntityDto.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +class AssociatedEntityDto +{ + public $singleId; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderPostgresTest.php b/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderPostgresTest.php index e0c897ce23232..53cbbb07a211c 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderPostgresTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderPostgresTest.php @@ -10,6 +10,7 @@ /** * @requires extension pdo_pgsql + * * @group integration */ class DoctrineTokenProviderPostgresTest extends DoctrineTokenProviderTest diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index 451046f2ec150..c5a5592185085 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -21,6 +21,7 @@ use Doctrine\Persistence\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper; +use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociatedEntityDto; use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2; use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; @@ -609,6 +610,40 @@ public function testAssociatedEntityWithNull() $this->assertNoViolation(); } + public function testAssociatedEntityReferencedByPrimaryKey() + { + $this->registry = $this->createRegistryMock($this->em); + $this->registry->expects($this->any()) + ->method('getManagerForClass') + ->willReturn($this->em); + $this->validator = $this->createValidator(); + $this->validator->initialize($this->context); + + $entity = new SingleIntIdEntity(1, 'foo'); + $associated = new AssociationEntity(); + $associated->single = $entity; + + $this->em->persist($entity); + $this->em->persist($associated); + $this->em->flush(); + + $dto = new AssociatedEntityDto(); + $dto->singleId = 1; + + $this->validator->validate($dto, new UniqueEntity( + fields: ['singleId' => 'single'], + entityClass: AssociationEntity::class, + )); + + $this->buildViolation('This value is already used.') + ->atPath('property.path.single') + ->setParameter('{{ value }}', 1) + ->setInvalidValue(1) + ->setCode(UniqueEntity::NOT_UNIQUE_ERROR) + ->setCause([$associated]) + ->assertRaised(); + } + public function testValidateUniquenessWithArrayValue() { $repository = $this->createRepositoryMock(SingleIntIdEntity::class); diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php index 9a0ecc9d97781..3b8196fae410e 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php @@ -21,7 +21,8 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\NameExpression; -use Twig\Node\Expression\Variable\LocalVariable; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\Node\Nodes; @@ -34,7 +35,6 @@ final class TranslationDefaultDomainNodeVisitor implements NodeVisitorInterface { private Scope $scope; - private int $nestingLevel = 0; public function __construct() { @@ -48,22 +48,25 @@ public function enterNode(Node $node, Environment $env): Node } if ($node instanceof TransDefaultDomainNode) { - ++$this->nestingLevel; - if ($node->getNode('expr') instanceof ConstantExpression) { $this->scope->set('domain', $node->getNode('expr')); return $node; } + if (null === $templateName = $node->getTemplateName()) { + throw new \LogicException('Cannot traverse a node without a template name.'); + } + + $var = '__internal_trans_default_domain'.hash('xxh128', $templateName); + if (class_exists(Nodes::class)) { - $name = new LocalVariable(null, $node->getTemplateLine()); - $this->scope->set('domain', $name); + $name = new AssignContextVariable($var, $node->getTemplateLine()); + $this->scope->set('domain', new ContextVariable($var, $node->getTemplateLine())); return new SetNode(false, new Nodes([$name]), new Nodes([$node->getNode('expr')]), $node->getTemplateLine()); } - $var = '__internal_trans_default_domain_'.$this->nestingLevel; $name = new AssignNameExpression($var, $node->getTemplateLine()); $this->scope->set('domain', new NameExpression($var, $node->getTemplateLine())); @@ -105,8 +108,6 @@ public function enterNode(Node $node, Environment $env): Node public function leaveNode(Node $node, Environment $env): ?Node { if ($node instanceof TransDefaultDomainNode) { - --$this->nestingLevel; - return null; } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php index 96f707cdfdf2c..f6dd5f623baee 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php @@ -206,6 +206,68 @@ public function testDefaultTranslationDomainWithNamedArguments() $this->assertEquals('foo (custom)foo (foo)foo (custom)foo (custom)foo (fr)foo (custom)foo (fr)', trim($template->render([]))); } + public function testDefaultTranslationDomainWithExpression() + { + $templates = [ + 'index' => ' + {%- extends "base" %} + + {%- trans_default_domain custom_domain %} + + {%- block content %} + {{- "foo"|trans }} + {%- endblock %} + ', + + 'base' => ' + {%- block content "" %} + ', + ]; + + $translator = new Translator('en'); + $translator->addLoader('array', new ArrayLoader()); + $translator->addResource('array', ['foo' => 'foo (messages)'], 'en'); + $translator->addResource('array', ['foo' => 'foo (custom)'], 'en', 'custom'); + $translator->addResource('array', ['foo' => 'foo (foo)'], 'en', 'foo'); + + $template = $this->getTemplate($templates, $translator); + + $this->assertEquals('foo (foo)', trim($template->render(['custom_domain' => 'foo']))); + } + + public function testDefaultTranslationDomainWithExpressionAndInheritance() + { + $templates = [ + 'index' => ' + {%- extends "base" %} + + {%- trans_default_domain foo_domain %} + + {%- block content %} + {{- "foo"|trans }} + {%- endblock %} + ', + + 'base' => ' + {%- trans_default_domain custom_domain %} + + {{- "foo"|trans }} + {%- block content "" %} + {{- "foo"|trans }} + ', + ]; + + $translator = new Translator('en'); + $translator->addLoader('array', new ArrayLoader()); + $translator->addResource('array', ['foo' => 'foo (messages)'], 'en'); + $translator->addResource('array', ['foo' => 'foo (custom)'], 'en', 'custom'); + $translator->addResource('array', ['foo' => 'foo (foo)'], 'en', 'foo'); + + $template = $this->getTemplate($templates, $translator); + + $this->assertEquals('foo (custom)foo (foo)foo (custom)', trim($template->render(['foo_domain' => 'foo', 'custom_domain' => 'custom']))); + } + private function getTemplate($template, ?TranslatorInterface $translator = null): TemplateWrapper { $translator ??= new Translator('en'); diff --git a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php index dc419f5dad227..ab9113acf5c57 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php @@ -21,6 +21,7 @@ use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; use Twig\Node\Nodes; @@ -275,32 +276,32 @@ public function testCompileLabelWithLabelAndAttributes() public function testCompileLabelWithLabelThatEvaluatesToNull() { + if (class_exists(ConditionalTernary::class)) { + $conditional = new ConditionalTernary( + // if + new ConstantExpression(true, 0), + // then + new ConstantExpression(null, 0), + // else + new ConstantExpression(null, 0), + 0 + ); + } else { + $conditional = new ConditionalExpression( + // if + new ConstantExpression(true, 0), + // then + new ConstantExpression(null, 0), + // else + new ConstantExpression(null, 0), + 0 + ); + } + if (class_exists(Nodes::class)) { - $arguments = new Nodes([ - new ContextVariable('form', 0), - new ConditionalExpression( - // if - new ConstantExpression(true, 0), - // then - new ConstantExpression(null, 0), - // else - new ConstantExpression(null, 0), - 0 - ), - ]); + $arguments = new Nodes([new ContextVariable('form', 0), $conditional]); } else { - $arguments = new Node([ - new NameExpression('form', 0), - new ConditionalExpression( - // if - new ConstantExpression(true, 0), - // then - new ConstantExpression(null, 0), - // else - new ConstantExpression(null, 0), - 0 - ), - ]); + $arguments = new Node([new NameExpression('form', 0), $conditional]); } $node = new SearchAndRenderBlockNode(new TwigFunction('form_label'), $arguments, 0); @@ -322,18 +323,32 @@ public function testCompileLabelWithLabelThatEvaluatesToNull() public function testCompileLabelWithLabelThatEvaluatesToNullAndAttributes() { + if (class_exists(ConditionalTernary::class)) { + $conditional = new ConditionalTernary( + // if + new ConstantExpression(true, 0), + // then + new ConstantExpression(null, 0), + // else + new ConstantExpression(null, 0), + 0 + ); + } else { + $conditional = new ConditionalExpression( + // if + new ConstantExpression(true, 0), + // then + new ConstantExpression(null, 0), + // else + new ConstantExpression(null, 0), + 0 + ); + } + if (class_exists(Nodes::class)) { $arguments = new Nodes([ new ContextVariable('form', 0), - new ConditionalExpression( - // if - new ConstantExpression(true, 0), - // then - new ConstantExpression(null, 0), - // else - new ConstantExpression(null, 0), - 0 - ), + $conditional, new ArrayExpression([ new ConstantExpression('foo', 0), new ConstantExpression('bar', 0), @@ -344,12 +359,7 @@ public function testCompileLabelWithLabelThatEvaluatesToNullAndAttributes() } else { $arguments = new Node([ new NameExpression('form', 0), - new ConditionalExpression( - new ConstantExpression(true, 0), - new ConstantExpression(null, 0), - new ConstantExpression(null, 0), - 0 - ), + $conditional, new ArrayExpression([ new ConstantExpression('foo', 0), new ConstantExpression('bar', 0), diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php index f4281cd21eef8..4dc86130a8cc5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php @@ -86,7 +86,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ['Timezone', date_default_timezone_get().' ('.(new \DateTimeImmutable())->format(\DateTimeInterface::W3C).')'], ['OPcache', \extension_loaded('Zend OPcache') ? (filter_var(\ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed'], ['APCu', \extension_loaded('apcu') ? (filter_var(\ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed'], - ['Xdebug', \extension_loaded('xdebug') ? ($xdebugMode && $xdebugMode !== 'off' ? 'Enabled (' . $xdebugMode . ')' : 'Not enabled') : 'Not installed'], + ['Xdebug', \extension_loaded('xdebug') ? ($xdebugMode && 'off' !== $xdebugMode ? 'Enabled (' . $xdebugMode . ')' : 'Not enabled') : 'Not installed'], ]; $io->table([], $rows); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 26cae1f306c8f..a6d5956033d5e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -310,10 +310,17 @@ public function load(array $configs, ContainerBuilder $container): void } } + $emptySecretHint = '"framework.secret" option'; if (isset($config['secret'])) { $container->setParameter('kernel.secret', $config['secret']); + $usedEnvs = []; + $container->resolveEnvPlaceholders($config['secret'], null, $usedEnvs); + + if ($usedEnvs) { + $emptySecretHint = \sprintf('"%s" env var%s', implode('", "', $usedEnvs), 1 === \count($usedEnvs) ? '' : 's'); + } } - $container->parameterCannotBeEmpty('kernel.secret', 'A non-empty value for the parameter "kernel.secret" is required. Did you forget to configure the "framework.secret" option?'); + $container->parameterCannotBeEmpty('kernel.secret', 'A non-empty value for the parameter "kernel.secret" is required. Did you forget to configure the '.$emptySecretHint.'?'); $container->setParameter('kernel.http_method_override', $config['http_method_override']); $container->setParameter('kernel.trust_x_sendfile_type_header', $config['trust_x_sendfile_type_header']); @@ -2777,7 +2784,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $container->removeDefinition('notifier.channel.email'); } - foreach (['texter', 'chatter', 'notifier.channel.chat', 'notifier.channel.email', 'notifier.channel.sms'] as $serviceId) { + foreach (['texter', 'chatter', 'notifier.channel.chat', 'notifier.channel.email', 'notifier.channel.sms', 'notifier.channel.push', 'notifier.channel.desktop'] as $serviceId) { if (!$container->hasDefinition($serviceId)) { continue; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php index 04fa4ce510a2b..28900ad10d7bd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php @@ -75,11 +75,17 @@ ->tag('notifier.channel', ['channel' => 'email']) ->set('notifier.channel.push', PushChannel::class) - ->args([service('texter.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->args([ + service('texter.transports'), + abstract_arg('message bus'), + ]) ->tag('notifier.channel', ['channel' => 'push']) ->set('notifier.channel.desktop', DesktopChannel::class) - ->args([service('texter.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->args([ + service('texter.transports'), + abstract_arg('message bus'), + ]) ->tag('notifier.channel', ['channel' => 'desktop']) ->set('notifier.monolog_handler', NotifierHandler::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index 53856f356d056..6abe1b6d8f1a2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -157,6 +157,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] ->args([ new Parameter('kernel.secret'), ]) + ->lazy() ->alias(UriSigner::class, 'uri_signer') ->set('config_cache_factory', ResourceCheckerConfigCacheFactory::class) diff --git a/src/Symfony/Component/Cache/Traits/Relay/MoveTrait.php b/src/Symfony/Component/Cache/Traits/Relay/MoveTrait.php index 1f1b84c009399..18086f61d68c5 100644 --- a/src/Symfony/Component/Cache/Traits/Relay/MoveTrait.php +++ b/src/Symfony/Component/Cache/Traits/Relay/MoveTrait.php @@ -33,12 +33,12 @@ public function lmove($srckey, $dstkey, $srcpos, $dstpos): mixed */ trait MoveTrait { - public function blmove($srckey, $dstkey, $srcpos, $dstpos, $timeout): \Relay\Relay|false|null|string + public function blmove($srckey, $dstkey, $srcpos, $dstpos, $timeout): \Relay\Relay|false|string|null { return $this->initializeLazyObject()->blmove(...\func_get_args()); } - public function lmove($srckey, $dstkey, $srcpos, $dstpos): \Relay\Relay|false|null|string + public function lmove($srckey, $dstkey, $srcpos, $dstpos): \Relay\Relay|false|string|null { return $this->initializeLazyObject()->lmove(...\func_get_args()); } diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 2dac1dfa237b7..49e34d8d8c9e8 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -229,7 +229,7 @@ public function getEstimated(): float public function getRemaining(): float { - if (!$this->step) { + if (0 === $this->step || $this->step === $this->startingStep) { return 0; } diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 4693535051124..4f6e6cb96cf32 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -79,6 +79,7 @@ protected function tearDown(): void pcntl_signal(\SIGTERM, \SIG_DFL); pcntl_signal(\SIGUSR1, \SIG_DFL); pcntl_signal(\SIGUSR2, \SIG_DFL); + pcntl_signal(\SIGALRM, \SIG_DFL); } } diff --git a/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php b/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php index 9c04d2706e8da..408f8c0d35c58 100644 --- a/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php +++ b/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php @@ -39,6 +39,7 @@ protected function tearDown(): void pcntl_signal(\SIGTERM, \SIG_DFL); pcntl_signal(\SIGUSR1, \SIG_DFL); pcntl_signal(\SIGUSR2, \SIG_DFL); + pcntl_signal(\SIGALRM, \SIG_DFL); } } diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 88b3e9228a2f2..3d1bfa48fce27 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -110,6 +110,16 @@ public function testRegularTimeEstimation() ); } + public function testRegularTimeRemainingWithDifferentStartAtAndCustomDisplay() + { + $this->expectNotToPerformAssertions(); + + ProgressBar::setFormatDefinition('custom', ' %current%/%max% [%bar%] %percent:3s%% %remaining% %estimated%'); + $bar = new ProgressBar($this->getOutputStream(), 1_200, 0); + $bar->setFormat('custom'); + $bar->start(1_200, 600); + } + public function testResumedTimeEstimation() { $bar = new ProgressBar($output = $this->getOutputStream(), 1_200, 0); diff --git a/src/Symfony/Component/Console/Tests/SignalRegistry/SignalRegistryTest.php b/src/Symfony/Component/Console/Tests/SignalRegistry/SignalRegistryTest.php index f997f7c1d8cee..92d500f9ee4e5 100644 --- a/src/Symfony/Component/Console/Tests/SignalRegistry/SignalRegistryTest.php +++ b/src/Symfony/Component/Console/Tests/SignalRegistry/SignalRegistryTest.php @@ -27,6 +27,7 @@ protected function tearDown(): void pcntl_signal(\SIGTERM, \SIG_DFL); pcntl_signal(\SIGUSR1, \SIG_DFL); pcntl_signal(\SIGUSR2, \SIG_DFL); + pcntl_signal(\SIGALRM, \SIG_DFL); } public function testOneCallbackForASignalSignalIsHandled() diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt index cfb10d03dafdd..81becafd8e350 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt @@ -24,7 +24,7 @@ var_dump([ $eHandler[0]->setExceptionHandler('print_r'); if (true) { - class Broken implements \JsonSerializable + class Broken implements \Iterator { } } @@ -37,14 +37,14 @@ array(1) { } object(Symfony\Component\ErrorHandler\Error\FatalError)#%d (%d) { ["message":protected]=> - string(186) "Error: Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" + string(209) "Error: Class Symfony\Component\ErrorHandler\Broken contains 5 abstract methods and must therefore be declared abstract or implement the remaining methods (Iterator::current, Iterator::next, Iterator::key, ...)" %a ["error":"Symfony\Component\ErrorHandler\Error\FatalError":private]=> array(4) { ["type"]=> int(1) ["message"]=> - string(179) "Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" + string(202) "Class Symfony\Component\ErrorHandler\Broken contains 5 abstract methods and must therefore be declared abstract or implement the remaining methods (Iterator::current, Iterator::next, Iterator::key, ...)" ["file"]=> string(%d) "%s" ["line"]=> diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 88feb64caef81..67a6c1dddd5d8 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -197,13 +197,12 @@ public function request(string $method, string $url, array $options = []): Respo $curlopts[\CURLOPT_RESOLVE] = $resolve; } + $curlopts[\CURLOPT_CUSTOMREQUEST] = $method; if ('POST' === $method) { // Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303 $curlopts[\CURLOPT_POST] = true; } elseif ('HEAD' === $method) { $curlopts[\CURLOPT_NOBODY] = true; - } else { - $curlopts[\CURLOPT_CUSTOMREQUEST] = $method; } if ('\\' !== \DIRECTORY_SEPARATOR && $options['timeout'] < 1) { diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index 55c424011de84..119e201b204e1 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -316,7 +316,7 @@ private static function perform(ClientState $multi, ?array &$responses = null): } $multi->handlesActivity[$id][] = null; - $multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) || (curl_error($ch) === 'OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 0' && -1.0 === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) && \in_array('close', array_map('strtolower', $responses[$id]->headers['connection']), true)) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).\sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL))); + $multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) || ('OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 0' === curl_error($ch) && -1.0 === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) && \in_array('close', array_map('strtolower', $responses[$id]->headers['connection']), true)) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).\sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL))); } } finally { $multi->performing = false; diff --git a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php index 1a30f16c1ff0e..a18b253203092 100644 --- a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php @@ -17,6 +17,7 @@ /** * @requires extension curl + * * @group dns-sensitive */ class CurlHttpClientTest extends HttpClientTestCase diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index 5ec2ae442e10f..d3efa896db8ac 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -656,4 +656,26 @@ public function testDefaultContentType() $this->assertSame(['abc' => 'def', 'content-type' => 'application/json', 'REQUEST_METHOD' => 'POST'], $response->toArray()); } + + public function testHeadRequestWithClosureBody() + { + $p = TestHttpServer::start(8067); + + try { + $client = $this->getHttpClient(__FUNCTION__); + + $response = $client->request('HEAD', 'http://localhost:8057/head', [ + 'body' => fn () => '', + ]); + $headers = $response->getHeaders(); + } finally { + $p->stop(); + } + + $this->assertArrayHasKey('x-request-vars', $headers); + + $vars = json_decode($headers['x-request-vars'][0], true); + $this->assertIsArray($vars); + $this->assertSame('HEAD', $vars['REQUEST_METHOD']); + } } diff --git a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php index 181b7f42be28d..c160dff77492a 100644 --- a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php @@ -68,6 +68,7 @@ public static function getExcludeHostData(): iterable /** * @dataProvider getExcludeIpData + * * @group dns-sensitive */ public function testExcludeByIp(string $ipAddr, $subnets, bool $mustThrow) @@ -105,6 +106,7 @@ public function testExcludeByIp(string $ipAddr, $subnets, bool $mustThrow) /** * @dataProvider getExcludeHostData + * * @group dns-sensitive */ public function testExcludeByHost(string $ipAddr, $subnets, bool $mustThrow) diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json index f294e7a577902..7ca008fd01f13 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -25,7 +25,7 @@ "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "~3.4.3|^3.5.1", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, "require-dev": { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php index 747a495036d17..8713dcf1e55d9 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php @@ -57,7 +57,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep 'php_intl_locale' => class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', 'php_timezone' => date_default_timezone_get(), 'xdebug_enabled' => \extension_loaded('xdebug'), - 'xdebug_status' => \extension_loaded('xdebug') ? ($xdebugMode && $xdebugMode !== 'off' ? 'Enabled (' . $xdebugMode . ')' : 'Not enabled') : 'Not installed', + 'xdebug_status' => \extension_loaded('xdebug') ? ($xdebugMode && 'off' !== $xdebugMode ? 'Enabled (' . $xdebugMode . ')' : 'Not enabled') : 'Not installed', 'apcu_enabled' => \extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL), 'apcu_status' => \extension_loaded('apcu') ? (filter_var(\ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed', 'zend_opcache_enabled' => \extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL), diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index f37b506b2202c..82e6dbfbd0c79 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -73,11 +73,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.2.0'; - public const VERSION_ID = 70200; + public const VERSION = '7.2.1'; + public const VERSION_ID = 70201; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 2; - public const RELEASE_VERSION = 0; + public const RELEASE_VERSION = 1; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2025'; diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridSmtpTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridSmtpTransportTest.php new file mode 100644 index 0000000000000..77e5135c55cc4 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridSmtpTransportTest.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Sendgrid\Tests\Transport; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridSmtpTransport; + +class SendgridSmtpTransportTest extends TestCase +{ + /** + * @dataProvider getTransportData + */ + public function testToString(SendgridSmtpTransport $transport, string $expected) + { + $this->assertSame($expected, (string) $transport); + } + + public static function getTransportData() + { + return [ + [ + new SendgridSmtpTransport('KEY'), + 'smtps://smtp.sendgrid.net', + ], + [ + new SendgridSmtpTransport('KEY', null, null, 'eu'), + 'smtps://smtp.eu.sendgrid.net', + ], + ]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridSmtpTransport.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridSmtpTransport.php index d939359ff8bc0..c7b67685dab9f 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridSmtpTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridSmtpTransport.php @@ -22,7 +22,7 @@ class SendgridSmtpTransport extends EsmtpTransport { public function __construct(#[\SensitiveParameter] string $key, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null, private ?string $region = null) { - parent::__construct('null' !== $region ? \sprintf('smtp.%s.sendgrid.net', $region) : 'smtp.sendgrid.net', 465, true, $dispatcher, $logger); + parent::__construct(null !== $region ? \sprintf('smtp.%s.sendgrid.net', $region) : 'smtp.sendgrid.net', 465, true, $dispatcher, $logger); $this->setUsername('apikey'); $this->setPassword($key); diff --git a/src/Symfony/Component/Messenger/Attribute/AsMessage.php b/src/Symfony/Component/Messenger/Attribute/AsMessage.php index bc60ec032bff9..ee46bd5491fe1 100644 --- a/src/Symfony/Component/Messenger/Attribute/AsMessage.php +++ b/src/Symfony/Component/Messenger/Attribute/AsMessage.php @@ -23,7 +23,7 @@ public function __construct( /** * Name of the transports to which the message should be routed. */ - public null|string|array $transport = null, + public string|array|null $transport = null, ) { } } diff --git a/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php b/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php index f2d6e27066356..4c7b6080bdf46 100644 --- a/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php +++ b/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php @@ -24,7 +24,7 @@ final class FormDataPart extends AbstractMultipartPart { /** - * @param array $fields + * @param array $fields */ public function __construct( private array $fields = [], diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index be1a471d9d53a..340d3684dd3e4 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -714,8 +714,12 @@ private function isAllowedProperty(string $class, string $property, bool $writeA return false; } - if (\PHP_VERSION_ID >= 80400 && ($reflectionProperty->isProtectedSet() || $reflectionProperty->isPrivateSet())) { - return false; + if (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isProtectedSet()) { + return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PROTECTED); + } + + if (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isPrivateSet()) { + return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PRIVATE); } if (\PHP_VERSION_ID >= 80400 &&$reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) { diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 13e9db752b2f2..972091d3031f0 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -703,6 +703,51 @@ public function testAsymmetricVisibility() $this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate')); } + /** + * @requires PHP 8.4 + */ + public function testAsymmetricVisibilityAllowPublicOnly() + { + $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC); + + $this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate')); + } + + /** + * @requires PHP 8.4 + */ + public function testAsymmetricVisibilityAllowProtectedOnly() + { + $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PROTECTED); + + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate')); + } + + /** + * @requires PHP 8.4 + */ + public function testAsymmetricVisibilityAllowPrivateOnly() + { + $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PRIVATE); + + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate')); + $this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate')); + $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected')); + $this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate')); + } + /** * @requires PHP 8.4 */ @@ -718,6 +763,7 @@ public function testVirtualProperties() /** * @dataProvider provideAsymmetricVisibilityMutator + * * @requires PHP 8.4 */ public function testAsymmetricVisibilityMutator(string $property, string $readVisibility, string $writeVisibility) @@ -743,6 +789,7 @@ public static function provideAsymmetricVisibilityMutator(): iterable /** * @dataProvider provideVirtualPropertiesMutator + * * @requires PHP 8.4 */ public function testVirtualPropertiesMutator(string $property, string $readVisibility, string $writeVisibility) diff --git a/src/Symfony/Component/Scheduler/Tests/RecurringMessageTest.php b/src/Symfony/Component/Scheduler/Tests/RecurringMessageTest.php index d668b7d03b02b..1954d4f470402 100644 --- a/src/Symfony/Component/Scheduler/Tests/RecurringMessageTest.php +++ b/src/Symfony/Component/Scheduler/Tests/RecurringMessageTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Scheduler\Tests; use PHPUnit\Framework\TestCase; -use Random\Randomizer; use Symfony\Component\Scheduler\Exception\InvalidArgumentException; use Symfony\Component\Scheduler\RecurringMessage; @@ -22,13 +21,8 @@ public function testCanCreateHashedCronMessage() { $object = new DummyStringableMessage(); - if (class_exists(Randomizer::class)) { - $this->assertSame('30 0 * * *', (string) RecurringMessage::cron('#midnight', $object)->getTrigger()); - $this->assertSame('30 0 * * 3', (string) RecurringMessage::cron('#weekly', $object)->getTrigger()); - } else { - $this->assertSame('36 0 * * *', (string) RecurringMessage::cron('#midnight', $object)->getTrigger()); - $this->assertSame('36 0 * * 6', (string) RecurringMessage::cron('#weekly', $object)->getTrigger()); - } + $this->assertSame('30 0 * * *', (string) RecurringMessage::cron('#midnight', $object)->getTrigger()); + $this->assertSame('30 0 * * 3', (string) RecurringMessage::cron('#weekly', $object)->getTrigger()); } public function testHashedCronContextIsRequiredIfMessageIsNotStringable() diff --git a/src/Symfony/Component/Scheduler/Tests/Trigger/CronExpressionTriggerTest.php b/src/Symfony/Component/Scheduler/Tests/Trigger/CronExpressionTriggerTest.php index cf12a7ceccf52..a700372d4765c 100644 --- a/src/Symfony/Component/Scheduler/Tests/Trigger/CronExpressionTriggerTest.php +++ b/src/Symfony/Component/Scheduler/Tests/Trigger/CronExpressionTriggerTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Scheduler\Tests\Trigger; use PHPUnit\Framework\TestCase; -use Random\Randomizer; use Symfony\Component\Scheduler\Trigger\CronExpressionTrigger; class CronExpressionTriggerTest extends TestCase @@ -33,54 +32,28 @@ public function testHashedExpressionParsing(string $input, string $expected) public static function hashedExpressionProvider(): array { - if (class_exists(Randomizer::class)) { - return [ - ['# * * * *', '30 * * * *'], - ['# # * * *', '30 0 * * *'], - ['# # # * *', '30 0 25 * *'], - ['# # # # *', '30 0 25 10 *'], - ['# # # # #', '30 0 25 10 5'], - ['# # 1,15 1-11 *', '30 0 1,15 1-11 *'], - ['# # 1,15 * *', '30 0 1,15 * *'], - ['#hourly', '30 * * * *'], - ['#daily', '30 0 * * *'], - ['#weekly', '30 0 * * 3'], - ['#weekly@midnight', '30 0 * * 3'], - ['#monthly', '30 0 25 * *'], - ['#monthly@midnight', '30 0 25 * *'], - ['#yearly', '30 0 25 10 *'], - ['#yearly@midnight', '30 0 25 10 *'], - ['#annually', '30 0 25 10 *'], - ['#annually@midnight', '30 0 25 10 *'], - ['#midnight', '30 0 * * *'], - ['#(1-15) * * * *', '1 * * * *'], - ['#(1-15) * * * #(3-5)', '1 * * * 3'], - ['#(1-15) * # * #(3-5)', '1 * 17 * 5'], - ]; - } - return [ - ['# * * * *', '36 * * * *'], - ['# # * * *', '36 0 * * *'], - ['# # # * *', '36 0 14 * *'], - ['# # # # *', '36 0 14 3 *'], - ['# # # # #', '36 0 14 3 5'], - ['# # 1,15 1-11 *', '36 0 1,15 1-11 *'], - ['# # 1,15 * *', '36 0 1,15 * *'], - ['#hourly', '36 * * * *'], - ['#daily', '36 0 * * *'], - ['#weekly', '36 0 * * 6'], - ['#weekly@midnight', '36 0 * * 6'], - ['#monthly', '36 0 14 * *'], - ['#monthly@midnight', '36 0 14 * *'], - ['#yearly', '36 0 14 3 *'], - ['#yearly@midnight', '36 0 14 3 *'], - ['#annually', '36 0 14 3 *'], - ['#annually@midnight', '36 0 14 3 *'], - ['#midnight', '36 0 * * *'], - ['#(1-15) * * * *', '7 * * * *'], - ['#(1-15) * * * #(3-5)', '7 * * * 3'], - ['#(1-15) * # * #(3-5)', '7 * 1 * 5'], + ['# * * * *', '30 * * * *'], + ['# # * * *', '30 0 * * *'], + ['# # # * *', '30 0 25 * *'], + ['# # # # *', '30 0 25 10 *'], + ['# # # # #', '30 0 25 10 5'], + ['# # 1,15 1-11 *', '30 0 1,15 1-11 *'], + ['# # 1,15 * *', '30 0 1,15 * *'], + ['#hourly', '30 * * * *'], + ['#daily', '30 0 * * *'], + ['#weekly', '30 0 * * 3'], + ['#weekly@midnight', '30 0 * * 3'], + ['#monthly', '30 0 25 * *'], + ['#monthly@midnight', '30 0 25 * *'], + ['#yearly', '30 0 25 10 *'], + ['#yearly@midnight', '30 0 25 10 *'], + ['#annually', '30 0 25 10 *'], + ['#annually@midnight', '30 0 25 10 *'], + ['#midnight', '30 0 * * *'], + ['#(1-15) * * * *', '1 * * * *'], + ['#(1-15) * * * #(3-5)', '1 * * * 3'], + ['#(1-15) * # * #(3-5)', '1 * 17 * 5'], ]; } diff --git a/src/Symfony/Component/Security/Http/CHANGELOG.md b/src/Symfony/Component/Security/Http/CHANGELOG.md index 9cb40fc926f4a..e9985ad13192b 100644 --- a/src/Symfony/Component/Security/Http/CHANGELOG.md +++ b/src/Symfony/Component/Security/Http/CHANGELOG.md @@ -14,7 +14,7 @@ CHANGELOG * Add `#[IsCsrfTokenValid]` attribute * Add CAS 2.0 access token handler - * Make empty username or empty password on form login attempts return Bad Request (400) + * Make empty username or empty password on form login attempts throw `BadCredentialsException` 7.0 --- diff --git a/src/Symfony/Component/TypeInfo/README.md b/src/Symfony/Component/TypeInfo/README.md index b654e271a1c6b..d9b3a2e5bbf2f 100644 --- a/src/Symfony/Component/TypeInfo/README.md +++ b/src/Symfony/Component/TypeInfo/README.md @@ -15,6 +15,7 @@ composer require phpstan/phpdoc-parser # to support raw string resolving resolve('bool'); // returns a "bool" Type instance // Types can be instantiated thanks to static factories $type = Type::list(Type::nullable(Type::bool())); -// Type instances have several helper methods -$type->getBaseType(); // returns an "array" Type instance -$type->getCollectionKeyType(); // returns an "int" Type instance -$type->getCollectionValueType()->isNullable(); // returns true +// Type classes have their specific methods +Type::object(FooClass::class)->getClassName(); +Type::enum(FooEnum::class, Type::int())->getBackingType(); +Type::list(Type::int())->isList(); + +// Every type can be cast to string +(string) Type::generic(Type::object(Collection::class), Type::int()) // returns "Collection" + +// You can check that a type (or one of its wrapped/composed parts) is identified by one of some identifiers. +$type->isIdentifiedBy(Foo::class, Bar::class); +$type->isIdentifiedBy(TypeIdentifier::OBJECT); +$type->isIdentifiedBy('float'); + +// You can also check that a type satifies specific conditions +$type->isSatisfiedBy(fn (Type $type): bool => !$type->isNullable() && $type->isIdentifiedBy(TypeIdentifier::INT)); ``` Resources diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php index 9e8796d09b3c4..60a0ded22c648 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php @@ -195,6 +195,7 @@ public function testCreateNullable() { $this->assertEquals(new NullableType(new BuiltinType(TypeIdentifier::INT)), Type::nullable(Type::int())); $this->assertEquals(new NullableType(new BuiltinType(TypeIdentifier::INT)), Type::nullable(Type::nullable(Type::int()))); + $this->assertEquals(new BuiltinType(TypeIdentifier::MIXED), Type::nullable(Type::mixed())); $this->assertEquals( new NullableType(new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING))), diff --git a/src/Symfony/Component/TypeInfo/Type/UnionType.php b/src/Symfony/Component/TypeInfo/Type/UnionType.php index fd11214fc306f..91597e2089e3b 100644 --- a/src/Symfony/Component/TypeInfo/Type/UnionType.php +++ b/src/Symfony/Component/TypeInfo/Type/UnionType.php @@ -45,7 +45,7 @@ public function __construct(Type ...$types) } if ($type instanceof BuiltinType) { - if ($type->getTypeIdentifier() === TypeIdentifier::NULL && !is_a(static::class, NullableType::class, allow_string: true)) { + if (TypeIdentifier::NULL === $type->getTypeIdentifier() && !is_a(static::class, NullableType::class, allow_string: true)) { throw new InvalidArgumentException(\sprintf('Cannot create union with "null", please use "%s" instead.', NullableType::class)); } diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php index a1d7f8c43b461..d32a97276057c 100644 --- a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php +++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php @@ -330,11 +330,11 @@ public static function intersection(Type ...$types): IntersectionType * * @param T $type * - * @return ($type is NullableType ? T : NullableType) + * @return T|NullableType */ - public static function nullable(Type $type): NullableType + public static function nullable(Type $type): Type { - if ($type instanceof NullableType) { + if ($type->isNullable()) { return $type; } diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index 1a8cdfac570a4..4809e9b83bf7e 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -215,7 +215,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ }; } - if ($type instanceof BuiltinType && $type->getTypeIdentifier() !== TypeIdentifier::ARRAY && $type->getTypeIdentifier() !== TypeIdentifier::ITERABLE) { + if ($type instanceof BuiltinType && TypeIdentifier::ARRAY !== $type->getTypeIdentifier() && TypeIdentifier::ITERABLE !== $type->getTypeIdentifier()) { return $type; } diff --git a/src/Symfony/Component/TypeInfo/composer.json b/src/Symfony/Component/TypeInfo/composer.json index a10b6d8785d0e..a246dd94f45fd 100644 --- a/src/Symfony/Component/TypeInfo/composer.json +++ b/src/Symfony/Component/TypeInfo/composer.json @@ -29,12 +29,7 @@ "psr/container": "^1.1|^2.0" }, "require-dev": { - "phpstan/phpdoc-parser": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0" - }, - "conflict": { - "phpstan/phpdoc-parser": "<1.0", - "symfony/dependency-injection": "<6.4" + "phpstan/phpdoc-parser": "^1.0|^2.0" }, "autoload": { "psr-4": { "Symfony\\Component\\TypeInfo\\": "" }, diff --git a/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php b/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php index a75001785ae2d..59033d55a0350 100644 --- a/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php +++ b/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php @@ -42,6 +42,7 @@ exit; case '/head': + header('X-Request-Vars: '.json_encode($vars, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); header('Content-Length: '.strlen($json), true); break;