diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index f422cf3c55fab..3ac8d8025fa8e 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -29,9 +29,12 @@ '@Symfony' => true, '@Symfony:risky' => true, 'protected_to_private' => false, + 'trailing_comma_in_multiline' => ['elements' => ['arrays', 'match', 'parameters']], 'header_comment' => ['header' => $fileHeaderComment], + // TODO: Remove once the "compiler_optimized" set includes "sprintf" + 'native_function_invocation' => ['include' => ['@compiler_optimized', 'sprintf'], 'scope' => 'namespaced', 'strict' => true], 'nullable_type_declaration' => true, - 'trailing_comma_in_multiline' => ['elements' => ['arrays', 'match', 'parameters']], + 'nullable_type_declaration_for_default_null_value' => true, ]) ->setRiskyAllowed(true) ->setFinder( diff --git a/CHANGELOG-7.1.md b/CHANGELOG-7.1.md index 45a9e7fa2bbec..e78ae8f2b8b1c 100644 --- a/CHANGELOG-7.1.md +++ b/CHANGELOG-7.1.md @@ -7,6 +7,50 @@ in 7.1 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.1.0...v7.1.1 +* 7.1.2 (2024-06-28) + + * bug #57345 [DependencyInjection] Fix regression in ordering service locators by priority (longwave) + * bug #57553 [HttpKernel] Enable optional cache-warmers when cache-dir != build-dir (nicolas-grekas) + * bug #57497 [String] Fixed u()->snake(), b()->snake() and s()->snake() methods (arczinosek) + * bug #57574 [Filesystem] Fix Filesystem::remove() on Windows (nicolas-grekas) + * bug #57572 [DoctrineBridge] Fix compat with DI >= 6.4 (nicolas-grekas) + * bug #57541 [Serializer] [ObjectNormalizer] Use bool filter when FILTER_BOOL is set (Maximilian Zumbansen) + * bug #57538 [String] Add `alias` case to `EnglishInflector` (alexandre-daubois) + * bug #57533 [FrameworkBundle] Throw runtime exception when trying to use asset-mapper while http-client is disabled (nicolas-grekas) + * bug #57520 [SecurityBundle] Remove unused memory users’ `name` attribute from the XSD (MatTheCat) + * bug #57554 [FrameworkBundle] Fix warming up routes (nicolas-grekas) + * feature #57557 Ibexa is sponsoring Symfony 5.4, thanks to them! \o/ (nicolas-grekas) + * bug #57433 [Serializer] forward exceptions caught in the `AbstractObjectNormalizer` (HypeMC, xabbuh) + * bug #57569 [HttpClient][Mailer] Revert "Let curl handle transfer encoding", use HTTP/1.1 for Mailgun (nicolas-grekas) + * bug #57564 [HttpClient] Fix initializing InformationalChunk (nicolas-grekas) + * bug #57289 [DependencyInjection] Fix phpdoc for $calls in class Autoconfigure (ThomasTr) + * bug #57499 [Mailer] Add additional headers in Scaleway bridge (MrMicky-FR) + * bug #57488 [Security] check token in payload instead just request (eltharin) + * bug #57460 [VarExporter] fix contravariance problem with __unserialize() in lazy proxy (nikophil) + * bug #57397 [VarDumper] Fix FFI caster test (alexandre-daubois) + * bug #57453 [HttpClient] Fix parsing SSE (fancyweb) + * bug #57467 [SecurityBundle] Add `provider` XML attribute to the authenticators it’s missing from (MatTheCat) + * bug #57447 [Notifier] [Lox24] Fix request body format to JSON string (alebedev80) + * bug #57434 [FrameworkBundle] inject the missing logger service (xabbuh) + * bug #57384 [Notifier] Fix thread key in GoogleChat bridge (romain-jacquart) + * bug #57372 [HttpKernel][Security] Fix accessing session for stateless request (VincentLanglet) + * bug #57112 [Messenger] Handle `AMQPConnectionException` when publishing a message (jwage) + * bug #57341 [Serializer] properly handle invalid data for false/true types (xabbuh) + * bug #57187 [Serializer] Fix `ObjectNormalizer` with property path (HypeMC) + * bug #57355 [ErrorHandler] Fix rendered exception code highlighting on PHP 8.3 (tscni) + * bug #57310 [DependencyInjection] Fix ternary in `AutowireCallable` attribute (alamirault) + * bug #57405 [DoctrineBridge] fix handling of special "value" constraint option (xabbuh) + * bug #57273 [FrameworkBundle] Fix setting default context for certain normalizers (HypeMC) + * bug #57395 [Notifier]  send the recipient phone number as an array (xabbuh) + * bug #57378 [Security] Change to `BadCredentialsException` when empty username / password (llupa) + * bug #52699 [Serializer] [PropertyAccessor] Ignore non-collection interface generics (mtarld) + * bug #54634 [String] Fix #54611 pluralization of -on ending words + singularization of -a ending foreign words (Geordie, DesLynx) + * bug #57213 [Validator] [UniqueValidator] Use correct variable as parameter in (custom) error message (seho-nl, Sebastien Hoek) + * bug #54920 [Messenger] Comply with Amazon SQS requirements for message body (VincentLanglet) + * bug #57348 [Notifier][Brevo] change type from chatter to texter (xabbuh) + * bug #57321 [AssetMapper] fix npm version constraint conversion (Jean-Beru) + * bug #57320 [Serializer] avoid calling undefined built-in is_*() functions (xabbuh) + * 7.1.1 (2024-06-04) * bug #57110 [PhpUnitBridge] Fix error handler triggered outside of tests (HypeMC) diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md index 204b6f1c75f23..f5ba66f7c48ce 100644 --- a/UPGRADE-7.1.md +++ b/UPGRADE-7.1.md @@ -45,6 +45,7 @@ Cache ----- * Deprecate `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter` with Couchbase 3 instead + * The algorithm for the default cache namespace changed from SHA256 to XXH128 DependencyInjection ------------------- diff --git a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php index f7a2cac124d2b..302b1a23c77c9 100644 --- a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php +++ b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php @@ -58,6 +58,8 @@ function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) { } if (isset($this->fileMap[$name])) { $wrappedInstance = $this->load($this->fileMap[$name], false); + } elseif ((new \ReflectionMethod($this, $this->methodMap[$name]))->isStatic()) { + $wrappedInstance = $this->{$this->methodMap[$name]}($this, false); } else { $wrappedInstance = $this->{$this->methodMap[$name]}(false); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DummyManager.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DummyManager.php new file mode 100644 index 0000000000000..04e5a2acdd334 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DummyManager.php @@ -0,0 +1,69 @@ +register('foo', \stdClass::class)->setPublic(true); - $container->getDefinition('foo')->setLazy(true)->addTag('proxy', ['interface' => \stdClass::class]); + $container->register('foo', DummyManager::class)->setPublic(true); + $container->getDefinition('foo')->setLazy(true)->addTag('proxy', ['interface' => ObjectManager::class]); $container->compile(); $dumper = new PhpDumper($container); @@ -46,8 +48,8 @@ public function testResetService() $registry->resetManager(); $this->assertSame($foo, $container->get('foo')); - $this->assertInstanceOf(\stdClass::class, $foo); - $this->assertFalse(property_exists($foo, 'bar')); + $this->assertInstanceOf(ObjectManager::class, $foo); + $this->assertFalse(isset($foo->bar)); } /** @@ -79,7 +81,7 @@ public function testResetServiceWillNotNestFurtherLazyServicesWithinEachOther() $service = $container->get('foo'); - self::assertInstanceOf(\stdClass::class, $service); + self::assertInstanceOf(ObjectManager::class, $service); self::assertInstanceOf(LazyObjectInterface::class, $service); self::assertFalse($service->isLazyObjectInitialized()); @@ -92,7 +94,7 @@ public function testResetServiceWillNotNestFurtherLazyServicesWithinEachOther() $service->initializeLazyObject(); $wrappedValue = $service->initializeLazyObject(); - self::assertInstanceOf(\stdClass::class, $wrappedValue); + self::assertInstanceOf(DummyManager::class, $wrappedValue); self::assertNotInstanceOf(LazyObjectInterface::class, $wrappedValue); } @@ -104,10 +106,10 @@ private function dumpLazyServiceDoctrineBridgeContainerAsFiles() $container = new ContainerBuilder(); - $container->register('foo', \stdClass::class) + $container->register('foo', DummyManager::class) ->setPublic(true) ->setLazy(true) - ->addTag('proxy', ['interface' => \stdClass::class]); + ->addTag('proxy', ['interface' => ObjectManager::class]); $container->compile(); $fileSystem = new Filesystem(); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php index 4380bba494bba..fbfc2cb39b4ed 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php @@ -60,6 +60,13 @@ public function testAttributeWithGroupsAndPaylod() self::assertSame('some attached data', $constraint->payload); self::assertSame(['some_group'], $constraint->groups); } + + public function testValueOptionConfiguresFields() + { + $constraint = new UniqueEntity(['value' => 'email']); + + $this->assertSame('email', $constraint->fields); + } } #[UniqueEntity(['email'], message: 'myMessage')] diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php index edc9011b27ea1..34a16df0efce8 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php @@ -60,7 +60,7 @@ public function __construct( $payload = null, array $options = [], ) { - if (\is_array($fields) && \is_string(key($fields)) && [] === array_diff(array_keys($fields), array_keys(get_class_vars(static::class)))) { + if (\is_array($fields) && \is_string(key($fields)) && [] === array_diff(array_keys($fields), array_merge(array_keys(get_class_vars(static::class)), ['value']))) { $options = array_merge($fields, $options); } else { $options['fields'] = $fields; diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index 13c9efdf0e65b..8f000592c5b51 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\PhpUnit; +use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestResult; use PHPUnit\Runner\ErrorHandler; use PHPUnit\Util\Error\Handler; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 437d5402ae9a2..fd57cc95e6a5f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -344,7 +344,7 @@ public function load(array $configs, ContainerBuilder $container): void throw new LogicException('AssetMapper support cannot be enabled as the AssetMapper component is not installed. Try running "composer require symfony/asset-mapper".'); } - $this->registerAssetMapperConfiguration($config['asset_mapper'], $container, $loader, $this->readConfigEnabled('assets', $container, $config['assets'])); + $this->registerAssetMapperConfiguration($config['asset_mapper'], $container, $loader, $this->readConfigEnabled('assets', $container, $config['assets']), $this->readConfigEnabled('http_client', $container, $config['http_client'])); } else { $container->removeDefinition('cache.asset_mapper'); } @@ -1291,12 +1291,14 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co } } - private function registerAssetMapperConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $assetEnabled): void + private function registerAssetMapperConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $assetEnabled, bool $httpClientEnabled): void { $loader->load('asset_mapper.php'); - if (!$assetEnabled) { - $container->removeDefinition('asset_mapper.asset_package'); + if (!$httpClientEnabled) { + $container->register('asset_mapper.http_client', HttpClientInterface::class) + ->addTag('container.error') + ->addError('You cannot use the AssetMapper integration since the HttpClient component is not enabled. Try enabling the "framework.http_client" config option.'); } $paths = $config['paths']; @@ -1905,18 +1907,20 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder } $arguments = $container->getDefinition('serializer.normalizer.object')->getArguments(); - $context = []; + $context = $arguments[6] ?? $defaultContext; if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) { - $context += ($arguments[6] ?? $defaultContext) + ['circular_reference_handler' => new Reference($config['circular_reference_handler'])]; + $context += ['circular_reference_handler' => new Reference($config['circular_reference_handler'])]; $container->getDefinition('serializer.normalizer.object')->setArgument(5, null); } if ($config['max_depth_handler'] ?? false) { - $context += ($arguments[6] ?? $defaultContext) + ['max_depth_handler' => new Reference($config['max_depth_handler'])]; + $context += ['max_depth_handler' => new Reference($config['max_depth_handler'])]; } $container->getDefinition('serializer.normalizer.object')->setArgument(6, $context); + + $container->getDefinition('serializer.normalizer.property')->setArgument(5, $defaultContext); } private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void @@ -2855,6 +2859,11 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ ->addArgument(new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE)); } + if (ContainerBuilder::willBeAvailable('symfony/bluesky-notifier', NotifierBridge\Bluesky\BlueskyTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier'])) { + $container->getDefinition($classToServices[NotifierBridge\Bluesky\BlueskyTransportFactory::class]) + ->addArgument(new Reference('logger')); + } + if (isset($config['admin_recipients'])) { $notifier = $container->getDefinition('notifier'); foreach ($config['admin_recipients'] as $i => $recipient) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php index b7ce65f030345..404e7af18d0a1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php @@ -54,6 +54,8 @@ ]) ->alias(AssetMapperInterface::class, 'asset_mapper') + ->alias('asset_mapper.http_client', 'http_client') + ->set('asset_mapper.mapped_asset_factory', MappedAssetFactory::class) ->args([ service('asset_mapper.public_assets_path_resolver'), @@ -197,7 +199,7 @@ ]) ->set('asset_mapper.importmap.resolver', JsDelivrEsmResolver::class) - ->args([service('http_client')]) + ->args([service('asset_mapper.http_client')]) ->set('asset_mapper.importmap.renderer', ImportMapRenderer::class) ->args([ @@ -212,12 +214,12 @@ ->set('asset_mapper.importmap.auditor', ImportMapAuditor::class) ->args([ service('asset_mapper.importmap.config_reader'), - service('http_client'), + service('asset_mapper.http_client'), ]) ->set('asset_mapper.importmap.update_checker', ImportMapUpdateChecker::class) ->args([ service('asset_mapper.importmap.config_reader'), - service('http_client'), + service('asset_mapper.http_client'), ]) ->set('asset_mapper.importmap.command.require', ImportMapRequireCommand::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index df9be94ed5e32..5ddc9ae240ea1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -27,7 +27,6 @@ $chatterFactories = [ 'bluesky' => Bridge\Bluesky\BlueskyTransportFactory::class, - 'brevo' => Bridge\Brevo\BrevoTransportFactory::class, 'chatwork' => Bridge\Chatwork\ChatworkTransportFactory::class, 'discord' => Bridge\Discord\DiscordTransportFactory::class, 'fake-chat' => Bridge\FakeChat\FakeChatTransportFactory::class, @@ -59,6 +58,7 @@ $texterFactories = [ 'all-my-sms' => Bridge\AllMySms\AllMySmsTransportFactory::class, 'bandwidth' => Bridge\Bandwidth\BandwidthTransportFactory::class, + 'brevo' => Bridge\Brevo\BrevoTransportFactory::class, 'click-send' => Bridge\ClickSend\ClickSendTransportFactory::class, 'clickatell' => Bridge\Clickatell\ClickatellTransportFactory::class, 'contact-everyone' => Bridge\ContactEveryone\ContactEveryoneTransportFactory::class, diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index 1135d37525340..c75776900d5b3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -140,7 +140,6 @@ service('property_info')->ignoreOnInvalid(), service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(), null, - [], ]) ->set('serializer.denormalizer.array', ArrayDenormalizer::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php index 4dfb71e747487..69428a1b70928 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php @@ -69,7 +69,7 @@ public function getRouteCollection(): RouteCollection $this->collection->addResource(new ContainerParametersResource($this->collectedParameters)); try { - $containerFile = ($this->paramFetcher)('kernel.cache_dir').'/'.($this->paramFetcher)('kernel.container_class').'.php'; + $containerFile = ($this->paramFetcher)('kernel.build_dir').'/'.($this->paramFetcher)('kernel.container_class').'.php'; if (file_exists($containerFile)) { $this->collection->addResource(new FileResource($containerFile)); } else { @@ -84,14 +84,12 @@ public function getRouteCollection(): RouteCollection public function warmUp(string $cacheDir, ?string $buildDir = null): array { - if (!$buildDir) { - return []; + if (null === $currentDir = $this->getOption('cache_dir')) { + return []; // skip warmUp when router doesn't use cache } - $currentDir = $this->getOption('cache_dir'); - - // force cache generation in build_dir - $this->setOption('cache_dir', $buildDir); + // force cache generation + $this->setOption('cache_dir', $buildDir ?? $cacheDir); $this->getMatcher(); $this->getGenerator(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractWebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractWebTestCase.php index 085cb812eba69..17ff5ed732971 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractWebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AbstractWebTestCase.php @@ -53,7 +53,7 @@ protected static function getKernelClass(): string protected static function createKernel(array $options = []): KernelInterface { - $class = self::getKernelClass(); + $class = static::getKernelClass(); if (!isset($options['test_case'])) { throw new \InvalidArgumentException('The option "test_case" must be set.'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php index 2856816d187a1..9d75c5bf675a2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php @@ -12,6 +12,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\TranslatableBackedEnum; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\AppKernel; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; /** * @author Kévin Dunglas @@ -35,39 +38,58 @@ public function testDeserializeArrayOfObject() $this->assertEquals($expected, $result); } - /** - * @dataProvider provideNormalizersAndEncodersWithDefaultContextOption - */ - public function testNormalizersAndEncodersUseDefaultContextConfigOption(string $normalizerId) + public function testNormalizersAndEncodersUseDefaultContextConfigOption() { - static::bootKernel(['test_case' => 'Serializer']); + /** @var SerializerKernel $kernel */ + $kernel = static::bootKernel(['test_case' => 'Serializer', 'root_config' => 'default_context.yaml']); + + foreach ($kernel->normalizersAndEncoders as $normalizerOrEncoderId) { + if (!static::getContainer()->has($normalizerOrEncoderId)) { + continue; + } + + $normalizerOrEncoder = static::getContainer()->get($normalizerOrEncoderId); - $normalizer = static::getContainer()->get($normalizerId); + $reflectionObject = new \ReflectionObject($normalizerOrEncoder); + $property = $reflectionObject->getProperty('defaultContext'); - $reflectionObject = new \ReflectionObject($normalizer); - $property = $reflectionObject->getProperty('defaultContext'); + $defaultContext = $property->getValue($normalizerOrEncoder); - $defaultContext = $property->getValue($normalizer); + self::assertArrayHasKey('fake_context_option', $defaultContext); + self::assertEquals('foo', $defaultContext['fake_context_option']); + } + } - self::assertArrayHasKey('fake_context_option', $defaultContext); - self::assertEquals('foo', $defaultContext['fake_context_option']); + protected static function getKernelClass(): string + { + return SerializerKernel::class; } +} + +class SerializerKernel extends AppKernel implements CompilerPassInterface +{ + public $normalizersAndEncoders = [ + 'serializer.normalizer.property.alias', // Special case as this normalizer isn't tagged + ]; - public static function provideNormalizersAndEncodersWithDefaultContextOption(): array + public function process(ContainerBuilder $container): void { - return [ - ['serializer.normalizer.constraint_violation_list.alias'], - ['serializer.normalizer.dateinterval.alias'], - ['serializer.normalizer.datetime.alias'], - ['serializer.normalizer.json_serializable.alias'], - ['serializer.normalizer.problem.alias'], - ['serializer.normalizer.uid.alias'], - ['serializer.normalizer.translatable.alias'], - ['serializer.normalizer.object.alias'], - ['serializer.encoder.xml.alias'], - ['serializer.encoder.yaml.alias'], - ['serializer.encoder.csv.alias'], - ]; + $services = array_merge( + $container->findTaggedServiceIds('serializer.normalizer'), + $container->findTaggedServiceIds('serializer.encoder') + ); + foreach ($services as $serviceId => $attributes) { + $class = $container->getDefinition($serviceId)->getClass(); + if (null === $reflectionConstructor = (new \ReflectionClass($class))->getConstructor()) { + continue; + } + foreach ($reflectionConstructor->getParameters() as $reflectionParam) { + if ('array $defaultContext' === $reflectionParam->getType()->getName().' $'.$reflectionParam->getName()) { + $this->normalizersAndEncoders[] = $serviceId.'.alias'; + break; + } + } + } } public function testSerializeTranslatableBackedEnum() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php index 06dc2d637a8e5..2fdbaea0fd9e8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php @@ -49,6 +49,11 @@ public function __construct($varDir, $testCase, $rootConfig, $environment, $debu parent::__construct($environment, $debug); } + protected function getContainerClass(): string + { + return parent::getContainerClass().substr(md5($this->rootConfig), -16); + } + public function registerBundles(): iterable { if (!file_exists($filename = $this->getProjectDir().'/'.$this->testCase.'/bundles.php')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml index 987cc384c7207..2f20dab9e8bc3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml @@ -10,7 +10,6 @@ framework: max_depth_handler: Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serializer\MaxDepthHandler default_context: enable_max_depth: true - fake_context_option: foo property_info: { enabled: true } services: @@ -18,54 +17,6 @@ services: alias: serializer public: true - serializer.normalizer.constraint_violation_list.alias: - alias: serializer.normalizer.constraint_violation_list - public: true - - serializer.normalizer.dateinterval.alias: - alias: serializer.normalizer.dateinterval - public: true - - serializer.normalizer.datetime.alias: - alias: serializer.normalizer.datetime - public: true - - serializer.normalizer.json_serializable.alias: - alias: serializer.normalizer.json_serializable - public: true - - serializer.normalizer.problem.alias: - alias: serializer.normalizer.problem - public: true - - serializer.normalizer.uid.alias: - alias: serializer.normalizer.uid - public: true - - serializer.normalizer.translatable.alias: - alias: serializer.normalizer.translatable - public: true - - serializer.normalizer.property.alias: - alias: serializer.normalizer.property - public: true - - serializer.normalizer.object.alias: - alias: serializer.normalizer.object - public: true - - serializer.encoder.xml.alias: - alias: serializer.encoder.xml - public: true - - serializer.encoder.yaml.alias: - alias: serializer.encoder.yaml - public: true - - serializer.encoder.csv.alias: - alias: serializer.encoder.csv - public: true - Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serializer\CircularReferenceHandler: ~ Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serializer\MaxDepthHandler: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/default_context.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/default_context.yaml new file mode 100644 index 0000000000000..de6114c5d4bb8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/default_context.yaml @@ -0,0 +1,59 @@ +imports: + - { resource: ../config/default.yml } + +framework: + serializer: + enabled: true + circular_reference_handler: ~ # This must be null + max_depth_handler: ~ # This must be null + default_context: + fake_context_option: foo + +services: + serializer.normalizer.constraint_violation_list.alias: + alias: serializer.normalizer.constraint_violation_list + public: true + + serializer.normalizer.dateinterval.alias: + alias: serializer.normalizer.dateinterval + public: true + + serializer.normalizer.datetime.alias: + alias: serializer.normalizer.datetime + public: true + + serializer.normalizer.json_serializable.alias: + alias: serializer.normalizer.json_serializable + public: true + + serializer.normalizer.object.alias: + alias: serializer.normalizer.object + public: true + + serializer.normalizer.problem.alias: + alias: serializer.normalizer.problem + public: true + + serializer.normalizer.property.alias: + alias: serializer.normalizer.property + public: true + + serializer.normalizer.uid.alias: + alias: serializer.normalizer.uid + public: true + + serializer.encoder.csv.alias: + alias: serializer.encoder.csv + public: true + + serializer.encoder.json.alias: + alias: serializer.encoder.json + public: true + + serializer.encoder.xml.alias: + alias: serializer.encoder.xml + public: true + + serializer.encoder.yaml.alias: + alias: serializer.encoder.yaml + public: true diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd index f5b69c7e5615c..a0806817a186c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd @@ -112,7 +112,6 @@ - @@ -206,6 +205,7 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php index d422675377afa..c5f04511752f1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php @@ -44,7 +44,7 @@ private function getForFirewall(): object if (!$this->locator->has($firewallName)) { $message = 'No '.$serviceIdentifier.' found for this firewall.'; if (\defined(static::class.'::FIREWALL_OPTION')) { - $message .= sprintf('Did you forget to add a "'.static::FIREWALL_OPTION.'" key under your "%s" firewall?', $firewallName); + $message .= sprintf(' Did you forget to add a "'.static::FIREWALL_OPTION.'" key under your "%s" firewall?', $firewallName); } throw new \LogicException($message); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml index 66dd30ea8d26a..f54c5064de23b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml @@ -64,9 +64,8 @@ - + - app.user_checker ROLE_USER diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml index 52a64d2f42908..e2f0e9865c251 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml @@ -15,7 +15,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml index a61d597fad573..e7f3e6873dfa8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml @@ -15,7 +15,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml index 1ba3c5e5098e4..462136c682cc5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml @@ -15,7 +15,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml index 314f25d263d71..cb82f2cc509f4 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml @@ -15,7 +15,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml index 6b51f236a50a7..2e0e75eabcb37 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml @@ -22,7 +22,6 @@ - diff --git a/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php b/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php index c74744c4f13d2..1701dd1aabedd 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php @@ -126,10 +126,10 @@ public function fileExcerpt(string $file, int $line, int $srcContext = 3): ?stri if (\PHP_VERSION_ID >= 80300) { // remove main pre/code tags $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code); - // split multiline code tags - $code = preg_replace_callback('#]++)>((?:[^<]*+\\n)++[^<]*+)#', fn ($m) => "".str_replace("\n", "\n", $m[2]).'', $code); - // Convert spaces to html entities to preserve indentation when rendered - $code = str_replace(' ', ' ', $code); + // split multiline span tags + $code = preg_replace_callback('#]++)>((?:[^<\\n]*+\\n)++[^<]*+)#', function ($m) { + return "".str_replace("\n", "\n", $m[2]).''; + }, $code); $content = explode("\n", $code); } else { // remove main code/span tags diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php index 65957f74e4fd9..3b3d6f857070f 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php @@ -28,8 +28,8 @@ class ImportMapRenderer { // https://generator.jspm.io/#S2NnYGAIzSvJLMlJTWEAAMYOgCAOAA - private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL = 'https://ga.jspm.io/npm:es-module-shims@1.8.2/dist/es-module-shims.js'; - private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_INTEGRITY = 'sha384-+dzlBT6NPToF0UZu7ZUA6ehxHY8h/TxJOZxzNXKhFD+5He5Hbex+0AIOiSsEaokw'; + private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL = 'https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js'; + private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_INTEGRITY = 'sha384-ie1x72Xck445i0j4SlNJ5W5iGeL3Dpa0zD48MZopgWsjNB/lt60SuG1iduZGNnJn'; public function __construct( private readonly ImportMapGenerator $importMapGenerator, diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapVersionChecker.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapVersionChecker.php index b0af5736eb821..6a2cf579956bd 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapVersionChecker.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapVersionChecker.php @@ -137,7 +137,7 @@ public static function convertNpmConstraint(string $versionConstraint): ?string if (str_contains($segment, '-') && !preg_match('/-(alpha|beta|rc)\./', $segment)) { // This is a range [$start, $end] = explode('-', $segment); - $processedSegments[] = '>='.self::cleanVersionSegment(trim($start)).' <='.self::cleanVersionSegment(trim($end)); + $processedSegments[] = self::cleanVersionSegment(trim($start)).' - '.self::cleanVersionSegment(trim($end)); } elseif (preg_match('/^~(\d+\.\d+)$/', $segment, $matches)) { // Handle the tilde when only major.minor specified $baseVersion = $matches[1]; diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapVersionCheckerTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapVersionCheckerTest.php index 43346d3de33aa..2d6582c1d6cc4 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapVersionCheckerTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapVersionCheckerTest.php @@ -261,6 +261,26 @@ public static function getCheckVersionsTests() new PackageVersionProblem('foo', 'bar', 'some/repo', '1.5.0'), ], ]; + + yield 'single with range constraint but no problem' => [ + [ + self::createRemoteEntry('foo', version: '1.0'), + self::createRemoteEntry('bar', version: '2.0.3'), + ], + [ + 'foo' => ['bar'], + 'bar' => [], + ], + [ + [ + 'url' => '/foo/1.0', + 'response' => [ + 'dependencies' => ['bar' => '1.11 - 2'], + ], + ], + ], + [], + ]; } /** @@ -297,22 +317,22 @@ public static function getNpmSpecificVersionConstraints() // Hyphen Ranges yield 'hyphen range simple' => [ '1.0.0 - 2.0.0', - '>=1.0.0 <=2.0.0', + '1.0.0 - 2.0.0', ]; yield 'hyphen range with v prefix' => [ 'v1.0.0 - 2.0.0', - '>=1.0.0 <=2.0.0', + '1.0.0 - 2.0.0', ]; yield 'hyphen range without patch' => [ '1.0 - 2.0', - '>=1.0 <=2.0', + '1.0 - 2.0', ]; yield 'hyphen range with no spaces' => [ '1.0-v2.0', - '>=1.0 <=2.0', + '1.0 - 2.0', ]; // .x Wildcards @@ -386,7 +406,7 @@ public static function getNpmSpecificVersionConstraints() yield 'multiple constraints with space and or operator' => [ '1.2.7 || 1.2.9- v2.0.0', - '1.2.7 || >=1.2.9 <=2.0.0', + '1.2.7 || 1.2.9 - 2.0.0', ]; yield 'tilde constraint with patch version no change' => [ diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index a6b391de1f25a..cab9bf61c88cb 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Deprecate `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter` * Add support for URL encoded characters in Couchbase DSN * Add support for using DSN with PDOAdapter + * The algorithm for the default cache namespace changed from SHA256 to XXH128 7.0 --- diff --git a/src/Symfony/Component/Console/Completion/CompletionInput.php b/src/Symfony/Component/Console/Completion/CompletionInput.php index 7ba41c0839da4..79c2f659a92c2 100644 --- a/src/Symfony/Component/Console/Completion/CompletionInput.php +++ b/src/Symfony/Component/Console/Completion/CompletionInput.php @@ -53,7 +53,7 @@ public static function fromString(string $inputStr, int $currentIndex): self * Create an input based on an COMP_WORDS token list. * * @param string[] $tokens the set of split tokens (e.g. COMP_WORDS or argv) - * @param $currentIndex the index of the cursor (e.g. COMP_CWORD) + * @param int $currentIndex the index of the cursor (e.g. COMP_CWORD) */ public static function fromTokens(array $tokens, int $currentIndex): self { diff --git a/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php b/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php index 5b6a8e42de94f..df0d081fd9acb 100644 --- a/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php +++ b/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php @@ -132,4 +132,19 @@ public static function provideFromStringData() yield ['bin/console cache:clear "multi word string"', ['bin/console', 'cache:clear', '"multi word string"']]; yield ['bin/console cache:clear \'multi word string\'', ['bin/console', 'cache:clear', '\'multi word string\'']]; } + + public function testToString() + { + $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 0); + $this->assertSame('foo| bar baz', (string) $input); + + $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 1); + $this->assertSame('foo bar| baz', (string) $input); + + $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 2); + $this->assertSame('foo bar baz|', (string) $input); + + $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 11); + $this->assertSame('foo bar baz |', (string) $input); + } } diff --git a/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php b/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php index 637a24e3ba1b6..dc2c84ca29a5e 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php @@ -21,7 +21,7 @@ class Autoconfigure { /** * @param array>|string[]|null $tags The tags to add to the service - * @param array>|null $calls The calls to be made when instantiating the service + * @param array>|null $calls The calls to be made when instantiating the service * @param array|null $bind The bindings to declare for the service * @param bool|string|null $lazy Whether the service is lazy-loaded * @param bool|null $public Whether to declare the service as public diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php b/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php index af0c8c46cf76e..bb17be5757276 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php @@ -45,7 +45,7 @@ public function __construct( public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition { - return (new Definition($type = \is_string($this->lazy) ? $this->lazy : ($type ?: 'Closure'))) + return (new Definition($type = \is_array($this->lazy) ? current($this->lazy) : ($type ?: 'Closure'))) ->setFactory(['Closure', 'fromCallable']) ->setArguments([\is_array($value) ? $value + [1 => '__invoke'] : $value]) ->setLazy($this->lazy || 'Closure' !== $type && 'callable' !== (string) $parameter->getType()); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php index 728feb0bd732f..032e905095c5d 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php @@ -131,7 +131,6 @@ public static function map(array $services): array $services[$k] = new ServiceClosureArgument($v); } - ksort($services); return $services; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireCallableTest.php b/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireCallableTest.php index f5aeb35d44939..9e1a0d85429ff 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireCallableTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireCallableTest.php @@ -93,4 +93,35 @@ public function testArrayCallableWithServiceOnly() self::assertEquals([new Reference('my_service'), '__invoke'], $attribute->value); self::assertFalse($attribute->lazy); } + + public function testLazyAsArrayInDefinition() + { + $attribute = new AutowireCallable(callable: [Foo::class, 'myMethod'], lazy: 'my_lazy_class'); + + self::assertSame([Foo::class, 'myMethod'], $attribute->value); + + $definition = $attribute->buildDefinition('my_value', 'my_custom_type', new \ReflectionParameter([Foo::class, 'myMethod'], 'myParameter')); + + self::assertSame('my_lazy_class', $definition->getClass()); + self::assertTrue($definition->isLazy()); + } + + public function testLazyIsFalseInDefinition() + { + $attribute = new AutowireCallable(callable: [Foo::class, 'myMethod'], lazy: false); + + self::assertFalse($attribute->lazy); + + $definition = $attribute->buildDefinition('my_value', 'my_custom_type', new \ReflectionParameter([Foo::class, 'myMethod'], 'myParameter')); + + self::assertSame('my_custom_type', $definition->getClass()); + self::assertFalse($definition->isLazy()); + } +} + +class Foo +{ + public function myMethod(callable $myParameter) + { + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php index 8d74e817356ab..5e6beda149f6f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php @@ -619,7 +619,7 @@ public function testTaggedLocatorWithDefaultPriorityMethodConfiguredViaAttribute // We need to check priority of instances in the factories $factories = (new \ReflectionClass($locator))->getProperty('factories'); - self::assertSame([BarTagClass::class, FooTagClass::class], array_keys($factories->getValue($locator))); + self::assertSame([FooTagClass::class, BarTagClass::class], array_keys($factories->getValue($locator))); } public function testTaggedLocatorWithDefaultIndexMethodAndWithDefaultPriorityMethodConfiguredViaAttribute() @@ -648,7 +648,7 @@ public function testTaggedLocatorWithDefaultIndexMethodAndWithDefaultPriorityMet // We need to check priority of instances in the factories $factories = (new \ReflectionClass($locator))->getProperty('factories'); - self::assertSame(['bar_tag_class', 'foo_tag_class'], array_keys($factories->getValue($locator))); + self::assertSame(['foo_tag_class', 'bar_tag_class'], array_keys($factories->getValue($locator))); self::assertSame($container->get(BarTagClass::class), $locator->get('bar_tag_class')); self::assertSame($container->get(FooTagClass::class), $locator->get('foo_tag_class')); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index 9e1d6065587b6..064f8f3395da7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -452,7 +452,7 @@ public static function getSubscribedServices(): array 'autowired' => new ServiceClosureArgument(new TypedReference('service.id', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'autowired', [new Autowire(service: 'service.id')])), 'autowired.nullable' => new ServiceClosureArgument(new TypedReference('service.id', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'autowired.nullable', [new Autowire(service: 'service.id')])), 'autowired.parameter' => new ServiceClosureArgument('foobar'), - 'autowire.decorated' => new ServiceClosureArgument(new Reference('.service_locator.4qmCWv..inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)), + 'autowire.decorated' => new ServiceClosureArgument(new Reference('.service_locator.PIYLhDv.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)), 'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'target', [new Target('someTarget')])), ]; $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php index faeb743162d11..812b47c7a6f1f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php @@ -214,7 +214,7 @@ public function testDefinitionOrderIsTheSame() $locator = $container->getDefinition($locator); $factories = $locator->getArguments()[0]; - static::assertSame(['service-1', 'service-2'], array_keys($factories)); + static::assertSame(['service-2', 'service-1'], array_keys($factories)); } public function testBindingsAreProcessed() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php index e0cc6382c121e..a1313528dc891 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php @@ -65,14 +65,14 @@ protected static function getTestServiceSubscriberService($container) protected static function getFooServiceService($container) { return $container->services['foo_service'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber((new \Symfony\Component\DependencyInjection\Argument\ServiceLocator($container->getService ??= $container->getService(...), [ - 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => ['privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'getCustomDefinitionService', false], 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => ['services', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', 'getTestServiceSubscriberService', false], + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => ['privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'getCustomDefinitionService', false], 'bar' => ['services', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', 'getTestServiceSubscriberService', false], 'baz' => ['privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'getCustomDefinitionService', false], 'late_alias' => ['services', 'late_alias', 'getLateAliasService', false], ], [ - 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'bar' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'baz' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'late_alias' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestDefinition1', diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php index 7cb7cd31296ab..810a87fe0d753 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php @@ -254,10 +254,10 @@ private function fileExcerpt(string $file, int $line, int $srcContext = 3): stri if (\PHP_VERSION_ID >= 80300) { // remove main pre/code tags $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code); - // split multiline code tags - $code = preg_replace_callback('#]++)>((?:[^<]*+\\n)++[^<]*+)#', fn ($m) => "".str_replace("\n", "\n", $m[2]).'', $code); - // Convert spaces to html entities to preserve indentation when rendered - $code = str_replace(' ', ' ', $code); + // split multiline span tags + $code = preg_replace_callback('#]++)>((?:[^<\\n]*+\\n)++[^<]*+)#', function ($m) { + return "".str_replace("\n", "\n", $m[2]).''; + }, $code); $content = explode("\n", $code); } else { // remove main code/span tags diff --git a/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css b/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css index ed90e6a073465..b40c58db7c87f 100644 --- a/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css +++ b/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css @@ -349,7 +349,7 @@ header .container { display: flex; justify-content: space-between; } .trace-code li { color: #969896; margin: 0; padding-left: 10px; float: left; width: 100%; } .trace-code li + li { margin-top: 5px; } .trace-code li.selected { background: var(--trace-selected-background); margin-top: 2px; } -.trace-code li code { color: var(--base-6); white-space: nowrap; } +.trace-code li code { color: var(--base-6); white-space: pre; } .trace-as-text .stacktrace { line-height: 1.8; margin: 0 0 15px; white-space: pre-wrap; } diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 2cd09747c2666..03e449d3013fd 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -164,7 +164,7 @@ private static function doRemove(array $files, bool $isRecursive): void } } elseif (is_dir($file)) { if (!$isRecursive) { - $tmpName = \dirname(realpath($file)).'/.'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-_')); + $tmpName = \dirname(realpath($file)).'/.!'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-!')); if (file_exists($tmpName)) { try { diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index fe5a890d96684..002e7d4c64a51 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -171,17 +171,17 @@ public function testCopyForOriginUrlsAndExistingLocalFileDefaultsToCopy() } $finder = new PhpExecutableFinder(); - $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', 'localhost:8057'])); + $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', 'localhost:8857'])); $process->setWorkingDirectory(__DIR__.'/Fixtures/web'); $process->start(); do { usleep(50000); - } while (!@fopen('http://localhost:8057', 'r')); + } while (!@fopen('http://localhost:8857', 'r')); try { - $sourceFilePath = 'http://localhost:8057/logo_symfony_header.png'; + $sourceFilePath = 'http://localhost:8857/logo_symfony_header.png'; $targetFilePath = $this->workspace.\DIRECTORY_SEPARATOR.'copy_target_file'; file_put_contents($targetFilePath, 'TARGET FILE'); $this->filesystem->copy($sourceFilePath, $targetFilePath, false); diff --git a/src/Symfony/Component/HttpClient/Chunk/InformationalChunk.php b/src/Symfony/Component/HttpClient/Chunk/InformationalChunk.php index 31ed1aa248d18..ca9eb470913f7 100644 --- a/src/Symfony/Component/HttpClient/Chunk/InformationalChunk.php +++ b/src/Symfony/Component/HttpClient/Chunk/InformationalChunk.php @@ -23,6 +23,8 @@ class InformationalChunk extends DataChunk public function __construct(int $statusCode, array $headers) { $this->status = [$statusCode, $headers]; + + parent::__construct(); } public function getInformationalStatus(): ?array diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index fb84b869e7a56..3e3c6622b1295 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -250,8 +250,9 @@ public function request(string $method, string $url, array $options = []): Respo if (isset($options['normalized_headers']['content-length'][0])) { $curlopts[\CURLOPT_INFILESIZE] = (int) substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: ')); - } elseif (!isset($options['normalized_headers']['transfer-encoding'])) { - $curlopts[\CURLOPT_INFILESIZE] = -1; + } + if (!isset($options['normalized_headers']['transfer-encoding'])) { + $curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding:'.(isset($curlopts[\CURLOPT_INFILESIZE]) ? '' : ' chunked'); } if ('POST' !== $method) { diff --git a/src/Symfony/Component/HttpClient/EventSourceHttpClient.php b/src/Symfony/Component/HttpClient/EventSourceHttpClient.php index 4e551ac0409f6..b5f88ddbabe8f 100644 --- a/src/Symfony/Component/HttpClient/EventSourceHttpClient.php +++ b/src/Symfony/Component/HttpClient/EventSourceHttpClient.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpClient; +use Symfony\Component\HttpClient\Chunk\DataChunk; use Symfony\Component\HttpClient\Chunk\ServerSentEvent; use Symfony\Component\HttpClient\Exception\EventSourceException; use Symfony\Component\HttpClient\Response\AsyncContext; @@ -121,17 +122,30 @@ public function request(string $method, string $url, array $options = []): Respo return; } - $rx = '/((?:\r\n){2,}|\r{2,}|\n{2,})/'; - $content = $state->buffer.$chunk->getContent(); - if ($chunk->isLast()) { - $rx = substr_replace($rx, '|$', -2, 0); + if ('' !== $content = $state->buffer) { + $state->buffer = ''; + yield new DataChunk(-1, $content); + } + + yield $chunk; + + return; } - $events = preg_split($rx, $content, -1, \PREG_SPLIT_DELIM_CAPTURE); + + $content = $state->buffer.$chunk->getContent(); + $events = preg_split('/((?:\r\n){2,}|\r{2,}|\n{2,})/', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); $state->buffer = array_pop($events); for ($i = 0; isset($events[$i]); $i += 2) { - $event = new ServerSentEvent($events[$i].$events[1 + $i]); + $content = $events[$i].$events[1 + $i]; + if (!preg_match('/(?:^|\r\n|[\r\n])[^:\r\n]/', $content)) { + yield new DataChunk(-1, $content); + + continue; + } + + $event = new ServerSentEvent($content); if ('' !== $event->getId()) { $context->setInfo('last_event_id', $state->lastEventId = $event->getId()); @@ -143,17 +157,6 @@ public function request(string $method, string $url, array $options = []): Respo yield $event; } - - if (preg_match('/^(?::[^\r\n]*+(?:\r\n|[\r\n]))+$/m', $state->buffer)) { - $content = $state->buffer; - $state->buffer = ''; - - yield $context->createChunk($content); - } - - if ($chunk->isLast()) { - yield $chunk; - } }); } } diff --git a/src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php index 629655392ce81..de199ac729a59 100644 --- a/src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php @@ -15,9 +15,11 @@ use Symfony\Component\HttpClient\Chunk\DataChunk; use Symfony\Component\HttpClient\Chunk\ErrorChunk; use Symfony\Component\HttpClient\Chunk\FirstChunk; +use Symfony\Component\HttpClient\Chunk\LastChunk; use Symfony\Component\HttpClient\Chunk\ServerSentEvent; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Component\HttpClient\Exception\EventSourceException; +use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpClient\Response\ResponseStream; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -34,7 +36,11 @@ class EventSourceHttpClientTest extends TestCase */ public function testGetServerSentEvents(string $sep) { - $rawData = <<assertSame(['Accept: text/event-stream', 'Cache-Control: no-cache'], $options['headers']); + + return new MockResponse([ + str_replace("\n", $sep, << false, 'http_method' => 'GET', 'url' => 'http://localhost:8080/events', 'response_headers' => ['content-type: text/event-stream']]); - $responseStream = new ResponseStream((function () use ($response, $chunk) { - yield $response => new FirstChunk(); - yield $response => $chunk; - yield $response => new ErrorChunk(0, 'timeout'); - })()); - - $hasCorrectHeaders = function ($options) { - $this->assertSame(['Accept: text/event-stream', 'Cache-Control: no-cache'], $options['headers']); - - return true; - }; - - $httpClient = $this->createMock(HttpClientInterface::class); - $httpClient->method('request')->with('GET', 'http://localhost:8080/events', $this->callback($hasCorrectHeaders))->willReturn($response); - - $httpClient->method('stream')->willReturn($responseStream); - - $es = new EventSourceHttpClient($httpClient); +TXT + ), + ], [ + 'canceled' => false, + 'http_method' => 'GET', + 'url' => 'http://localhost:8080/events', + 'response_headers' => ['content-type: text/event-stream'], + ]); + })); $res = $es->connect('http://localhost:8080/events'); $expected = [ new FirstChunk(), new ServerSentEvent(str_replace("\n", $sep, "event: builderror\nid: 46\ndata: {\"foo\": \"bar\"}\n\n")), new ServerSentEvent(str_replace("\n", $sep, "event: reload\nid: 47\ndata: {}\n\n")), - new ServerSentEvent(str_replace("\n", $sep, "event: reload\nid: 48\ndata: {}\n\n")), + new DataChunk(-1, str_replace("\n", $sep, ": this is a oneline comment\n\n")), + new DataChunk(-1, str_replace("\n", $sep, ": this is a\n: multiline comment\n\n")), + new ServerSentEvent(str_replace("\n", $sep, ": comments are ignored\nevent: reload\n: anywhere\nid: 48\ndata: {}\n\n")), new ServerSentEvent(str_replace("\n", $sep, "data: test\ndata:test\nid: 49\nevent: testEvent\n\n\n")), new ServerSentEvent(str_replace("\n", $sep, "id: 50\ndata: \ndata\ndata: \ndata\ndata: \n\n")), + new DataChunk(-1, str_replace("\n", $sep, "id: 60\ndata")), + new LastChunk("\r\n" === $sep ? 355 : 322), ]; - $i = 0; - - $this->expectExceptionMessage('Response has been canceled'); - while ($res) { - if ($i > 0) { - $res->cancel(); - } - foreach ($es->stream($res) as $chunk) { - if ($chunk->isTimeout()) { - continue; - } - - if ($chunk->isLast()) { - continue; - } - - $this->assertEquals($expected[$i++], $chunk); - } + foreach ($es->stream($res) as $chunk) { + $this->assertEquals(array_shift($expected), $chunk); } + $this->assertSame([], $expected); } public function testPostServerSentEvents() diff --git a/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php index 80ee950041384..88e879bb69c76 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php @@ -62,7 +62,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep $sessionMetadata = []; $sessionAttributes = []; $flashes = []; - if ($request->hasSession()) { + if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()) { $session = $request->getSession(); if ($session->isStarted()) { $sessionMetadata['Created'] = date(\DATE_RFC822, $session->getMetadataBag()->getCreated()); diff --git a/src/Symfony/Component/HttpKernel/Event/KernelEvent.php b/src/Symfony/Component/HttpKernel/Event/KernelEvent.php index efca16543044a..bc6643f0a4858 100644 --- a/src/Symfony/Component/HttpKernel/Event/KernelEvent.php +++ b/src/Symfony/Component/HttpKernel/Event/KernelEvent.php @@ -16,7 +16,7 @@ use Symfony\Contracts\EventDispatcher\Event; /** - * Base class for events thrown in the HttpKernel component. + * Base class for events dispatched in the HttpKernel component. * * @author Bernhard Schussek */ diff --git a/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php b/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php index f28cf9f514bcf..ecaaceb9488b8 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php @@ -91,7 +91,7 @@ public function onKernelResponse(ResponseEvent $event): void return; } - $session = $request->hasPreviousSession() ? $request->getSession() : null; + $session = !$request->attributes->getBoolean('_stateless') && $request->hasPreviousSession() ? $request->getSession() : null; if ($session instanceof Session) { $usageIndexValue = $usageIndexReference = &$session->getUsageIndex(); diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 0d96519f5dd0d..a7a0232ffa826 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.1.1'; - public const VERSION_ID = 70101; + public const VERSION = '7.1.2'; + public const VERSION_ID = 70102; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 1; - public const RELEASE_VERSION = 1; + public const RELEASE_VERSION = 2; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '01/2025'; @@ -528,10 +528,17 @@ protected function initializeContainer(): void touch($oldContainerDir.'.legacy'); } - $preload = $this instanceof WarmableInterface ? (array) $this->warmUp($this->container->getParameter('kernel.cache_dir'), $buildDir) : []; + $cacheDir = $this->container->getParameter('kernel.cache_dir'); + $preload = $this instanceof WarmableInterface ? (array) $this->warmUp($cacheDir, $buildDir) : []; if ($this->container->has('cache_warmer')) { - $preload = array_merge($preload, (array) $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir'), $buildDir)); + $cacheWarmer = $this->container->get('cache_warmer'); + + if ($cacheDir !== $buildDir) { + $cacheWarmer->enableOptionalWarmers(); + } + + $preload = array_merge($preload, (array) $cacheWarmer->warmUp($cacheDir, $buildDir)); } if ($preload && file_exists($preloadFile = $buildDir.'/'.$class.'.preload.php')) { diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php index 57f8f53b1e9f7..fdf550d0ecd41 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php @@ -40,8 +40,8 @@ public function testKernelTerminate() ->willReturn($profile); $kernel = $this->createMock(HttpKernelInterface::class); - $mainRequest = $this->createMock(Request::class); - $subRequest = $this->createMock(Request::class); + $mainRequest = new Request(); + $subRequest = new Request(); $response = $this->createMock(Response::class); $requestStack = new RequestStack(); diff --git a/src/Symfony/Component/Mailer/Bridge/Infobip/README.md b/src/Symfony/Component/Mailer/Bridge/Infobip/README.md index c86458c3991f9..b040f259e20b5 100644 --- a/src/Symfony/Component/Mailer/Bridge/Infobip/README.md +++ b/src/Symfony/Component/Mailer/Bridge/Infobip/README.md @@ -12,10 +12,24 @@ MAILER_DSN=infobip+api://KEY@BASE_URL MAILER_DSN=infobip+smtp://KEY@default ``` +Custom Headers +-------------- + +This transport supports the following custom headers: + +| Header | Type | Description | +| -------------------------------- | ------- | -------------------------------------------------------------------------------------------- | +| `X-Infobip-IntermediateReport` | boolean | The real-time Intermediate delivery report that will be sent on your callback server. | +| `X-Infobip-NotifyUrl` | string | The URL on your callback server on which the Delivery report will be sent. | +| `X-Infobip-NotifyContentType` | string | Preferred Delivery report content type. Can be application/json or application/xml. | +| `X-Infobip-MessageId` | string | The ID that uniquely identifies the message sent to a recipient. | +| `X-Infobip-Track` | boolean | Enable or disable open and click tracking. | + Resources --------- -* [Infobip Api Docs](https://www.infobip.com/docs/api#channels/email) -* [Contributing](https://symfony.com/doc/current/contributing/index.html) -* [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) + + * [Infobip Api Docs](https://www.infobip.com/docs/api#channels/email) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHttpTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHttpTransport.php index ca0535b6e37d1..98a283227c8c1 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHttpTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHttpTransport.php @@ -61,6 +61,7 @@ protected function doSendHttp(SentMessage $message): ResponseInterface $endpoint = sprintf('%s/v3/%s/messages.mime', $this->getEndpoint(), urlencode($this->domain)); $response = $this->client->request('POST', 'https://'.$endpoint, [ + 'http_version' => '1.1', 'auth_basic' => 'api:'.$this->key, 'headers' => $headers, 'body' => $body->bodyToIterable(), diff --git a/src/Symfony/Component/Mailer/Bridge/Scaleway/Tests/Transport/ScalewayApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Scaleway/Tests/Transport/ScalewayApiTransportTest.php index 0d91d002cd21c..f31e041ea73d0 100644 --- a/src/Symfony/Component/Mailer/Bridge/Scaleway/Tests/Transport/ScalewayApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Scaleway/Tests/Transport/ScalewayApiTransportTest.php @@ -69,6 +69,8 @@ public function testSend() $this->assertSame('attachment.txt', $body['attachments'][0]['name']); $this->assertSame('text/plain', $body['attachments'][0]['type']); $this->assertSame(base64_encode('some attachment'), $body['attachments'][0]['content']); + $this->assertSame('Reply-To', $body['additional_headers'][0]['key']); + $this->assertStringContainsString('foo@bar.fr', $body['additional_headers'][0]['value']); return new JsonMockResponse(['emails' => [['message_id' => 'foobar']]], [ 'http_code' => 200, @@ -81,6 +83,7 @@ public function testSend() $mail->subject('Hello!') ->to(new Address('saif.gmati@symfony.com', 'Saif Eddin')) ->from(new Address('fabpot@symfony.com', 'Fabien')) + ->replyTo(new Address('foo@bar.fr', 'Foo')) ->text('Hello There!') ->addPart(new DataPart('some attachment', 'attachment.txt', 'text/plain')); diff --git a/src/Symfony/Component/Mailer/Bridge/Scaleway/Transport/ScalewayApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Scaleway/Transport/ScalewayApiTransport.php index e3c64082588ea..a70e274b34c7b 100644 --- a/src/Symfony/Component/Mailer/Bridge/Scaleway/Transport/ScalewayApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Scaleway/Transport/ScalewayApiTransport.php @@ -99,6 +99,9 @@ private function getPayload(Email $email, Envelope $envelope): array if ($attachements = $this->prepareAttachments($email)) { $payload['attachments'] = $attachements; } + if ($headers = $this->getCustomHeaders($email)) { + $payload['additional_headers'] = $headers; + } return $payload; } @@ -120,6 +123,24 @@ private function prepareAttachments(Email $email): array return $attachments; } + private function getCustomHeaders(Email $email): array + { + $headers = []; + $headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender']; + foreach ($email->getHeaders()->all() as $name => $header) { + if (\in_array($name, $headersToBypass, true)) { + continue; + } + + $headers[] = [ + 'key' => $header->getName(), + 'value' => $header->getBodyAsString(), + ]; + } + + return $headers; + } + private function formatAddress(Address $address): array { $array = ['email' => $address->getAddress()]; diff --git a/src/Symfony/Component/Mailer/MailerInterface.php b/src/Symfony/Component/Mailer/MailerInterface.php index 8d9540a3e5e3f..ebac4b53efa4e 100644 --- a/src/Symfony/Component/Mailer/MailerInterface.php +++ b/src/Symfony/Component/Mailer/MailerInterface.php @@ -15,7 +15,7 @@ use Symfony\Component\Mime\RawMessage; /** - * Interface for mailers able to send emails synchronous and/or asynchronous. + * Interface for mailers able to send emails synchronously and/or asynchronously. * * Implementations must support synchronous and asynchronous sending. * diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsSenderTest.php b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsSenderTest.php index 80840c859cb05..d11a5d8037b27 100644 --- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsSenderTest.php +++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsSenderTest.php @@ -72,4 +72,19 @@ public function testSendWithAmazonSqsXrayTraceHeaderStamp() $sender = new AmazonSqsSender($connection, $serializer); $sender->send($envelope); } + + public function testSendEncodeBodyToRespectAmazonRequirements() + { + $envelope = new Envelope(new DummyMessage('Oy')); + $encoded = ['body' => "\x7", 'headers' => ['type' => DummyMessage::class]]; + + $connection = $this->createMock(Connection::class); + $connection->expects($this->once())->method('send')->with(base64_encode($encoded['body']), $encoded['headers']); + + $serializer = $this->createMock(SerializerInterface::class); + $serializer->method('encode')->with($envelope)->willReturn($encoded); + + $sender = new AmazonSqsSender($connection, $serializer); + $sender->send($envelope); + } } diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsSender.php b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsSender.php index 2430d015d477e..fabbdfcfa575b 100644 --- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsSender.php +++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsSender.php @@ -32,6 +32,7 @@ public function __construct( public function send(Envelope $envelope): Envelope { $encodedMessage = $this->serializer->encode($envelope); + $encodedMessage = $this->complyWithAmazonSqsRequirements($encodedMessage); /** @var DelayStamp|null $delayStamp */ $delayStamp = $envelope->last(DelayStamp::class); @@ -66,4 +67,20 @@ public function send(Envelope $envelope): Envelope return $envelope; } + + /** + * @see https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html + * + * @param array{body: string, headers?: array} $encodedMessage + * + * @return array{body: string, headers?: array} + */ + private function complyWithAmazonSqsRequirements(array $encodedMessage): array + { + if (preg_match('/[^\x20-\x{D7FF}\xA\xD\x9\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', $encodedMessage['body'])) { + $encodedMessage['body'] = base64_encode($encodedMessage['body']); + } + + return $encodedMessage; + } } diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrinePostgreSqlFilterIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrinePostgreSqlFilterIntegrationTest.php new file mode 100644 index 0000000000000..9a4738be2ed97 --- /dev/null +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrinePostgreSqlFilterIntegrationTest.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport; + +use Doctrine\DBAL\Configuration; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use Doctrine\DBAL\Schema\Sequence; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Tools\DsnParser; +use Doctrine\DBAL\Types\Type; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\PostgreSqlConnection; + +/** + * This test checks on a postgres connection whether the doctrine asset filter works as expected. + * + * @requires extension pdo_pgsql + * + * @group integration + */ +class DoctrinePostgreSqlFilterIntegrationTest extends TestCase +{ + private Connection $driverConnection; + + protected function setUp(): void + { + if (!$host = getenv('POSTGRES_HOST')) { + $this->markTestSkipped('Missing POSTGRES_HOST env variable'); + } + + $url = "pdo-pgsql://postgres:password@$host"; + $params = (new DsnParser())->parse($url); + $config = new Configuration(); + if (class_exists(DefaultSchemaManagerFactory::class)) { + $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + } + + $this->driverConnection = DriverManager::getConnection($params, $config); + + $this->createAssets(); + } + + protected function tearDown(): void + { + $this->removeAssets(); + + $this->driverConnection->close(); + } + + public function testFilterAssets() + { + $schemaManager = $this->driverConnection->createSchemaManager(); + + $this->assertFalse($schemaManager->tablesExist(['queue_table'])); + $this->assertTrue($schemaManager->tablesExist(['app_table'])); + $this->assertTrue($this->hasSequence('app_table_id')); + + $connection = new PostgreSqlConnection(['table_name' => 'queue_table'], $this->driverConnection); + $connection->setup(); + + $schemaManager = $this->driverConnection->createSchemaManager(); + + $this->assertTrue($schemaManager->tablesExist(['queue_table'])); + $this->assertTrue($schemaManager->tablesExist(['app_table'])); + $this->assertTrue($this->hasSequence('app_table_id')); + } + + private function createAssets(): void + { + $this->removeAssets(); + + $schemaManager = $this->driverConnection->createSchemaManager(); + $schemaManager->createTable(new Table('app_table', [new Column('id', Type::getType('integer'))])); + $schemaManager->createSequence(new Sequence('app_table_id')); + } + + private function removeAssets(): void + { + $schemaManager = $this->driverConnection->createSchemaManager(); + + if ($schemaManager->tablesExist(['queue_table'])) { + $schemaManager->dropTable('queue_table'); + } + + if ($schemaManager->tablesExist(['app_table'])) { + $schemaManager->dropTable('app_table'); + } + + if ($this->hasSequence('app_table_id')) { + $schemaManager->dropSequence('app_table_id'); + } + } + + private function hasSequence(string $name): bool + { + $schemaManager = $this->driverConnection->createSchemaManager(); + + $sequences = $schemaManager->listSequences(); + foreach ($sequences as $sequence) { + if ($sequence->getName() === $name) { + return true; + } + } + + return false; + } +} diff --git a/src/Symfony/Component/Messenger/EventListener/DispatchPcntlSignalListener.php b/src/Symfony/Component/Messenger/EventListener/DispatchPcntlSignalListener.php index 37b88ca1b852a..258ce64480344 100644 --- a/src/Symfony/Component/Messenger/EventListener/DispatchPcntlSignalListener.php +++ b/src/Symfony/Component/Messenger/EventListener/DispatchPcntlSignalListener.php @@ -21,6 +21,10 @@ class DispatchPcntlSignalListener implements EventSubscriberInterface { public function onWorkerRunning(): void { + if (!\function_exists('pcntl_signal_dispatch')) { + return; + } + pcntl_signal_dispatch(); } diff --git a/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php b/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php index ed74b3bc3fae2..4bcc9513a15b6 100644 --- a/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php +++ b/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php @@ -143,15 +143,15 @@ public function testFromPathWithUrl() } $finder = new PhpExecutableFinder(); - $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', 'localhost:8057'])); + $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', 'localhost:8856'])); $process->setWorkingDirectory(__DIR__.'/../Fixtures/web'); $process->start(); try { do { usleep(50000); - } while (!@fopen('http://localhost:8057', 'r')); - $p = DataPart::fromPath($file = 'http://localhost:8057/logo_symfony_header.png'); + } while (!@fopen('http://localhost:8856', 'r')); + $p = DataPart::fromPath($file = 'http://localhost:8856/logo_symfony_header.png'); $content = file_get_contents($file); $this->assertEquals($content, $p->getBody()); $maxLineLength = 76; diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php b/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php index dd162a3a80e22..2d2fdb59b950c 100644 --- a/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php @@ -61,7 +61,7 @@ protected function doSend(MessageInterface $message): SentMessage $options = []; $options['from'] = $message->getFrom() ?: $this->from; - $options['to'] = $message->getPhone(); + $options['to'] = [$message->getPhone()]; $options['text'] = $message->getSubject(); $response = $this->client->request('POST', $endpoint, [ diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php index 496128971798d..0c4ddba4c77f7 100644 --- a/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php @@ -87,15 +87,24 @@ protected function doSend(MessageInterface $message): SentMessage $threadKey = $options->getThreadKey() ?: $this->threadKey; + $threadKey = $options->getThreadKey() ?: $this->threadKey; + $url = sprintf('https://%s/v1/spaces/%s/messages?key=%s&token=%s%s', $this->getEndpoint(), $this->space, urlencode($this->accessKey), urlencode($this->accessToken), - $threadKey ? '&threadKey='.urlencode($threadKey) : '' + $threadKey ? '&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD' : '' ); + + $body = array_filter($options->toArray()); + + if ($threadKey) { + $body['thread']['threadKey'] = $threadKey; + } + $response = $this->client->request('POST', $url, [ - 'json' => array_filter($options->toArray()), + 'json' => $body, ]); try { diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php index 4f423290c3608..2b277f910b0b9 100644 --- a/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php @@ -108,11 +108,11 @@ public function testSendWithOptions() ->method('getContent') ->willReturn('{"name":"spaces/My-Space/messages/abcdefg.hijklmno"}'); - $expectedBody = json_encode(['text' => $message]); + $expectedBody = json_encode(['text' => $message, 'thread' => ['threadKey' => 'My-Thread']]); $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { $this->assertSame('POST', $method); - $this->assertSame('https://chat.googleapis.com/v1/spaces/My-Space/messages?key=theAccessKey&token=theAccessToken%3D&threadKey=My-Thread', $url); + $this->assertSame('https://chat.googleapis.com/v1/spaces/My-Space/messages?key=theAccessKey&token=theAccessToken%3D&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD', $url); $this->assertSame($expectedBody, $options['body']); return $response; diff --git a/src/Symfony/Component/Notifier/Bridge/Lox24/Lox24Transport.php b/src/Symfony/Component/Notifier/Bridge/Lox24/Lox24Transport.php index 31c71c8d9c6a6..19ac34b5edaf4 100644 --- a/src/Symfony/Component/Notifier/Bridge/Lox24/Lox24Transport.php +++ b/src/Symfony/Component/Notifier/Bridge/Lox24/Lox24Transport.php @@ -101,7 +101,7 @@ protected function doSend(MessageInterface $message): SentMessage 'Content-Type' => 'application/json', 'User-Agent' => 'LOX24 Symfony Notifier', ], - 'body' => $body, + 'json' => $body, ]); try { diff --git a/src/Symfony/Component/Notifier/Bridge/Lox24/README.md b/src/Symfony/Component/Notifier/Bridge/Lox24/README.md index 8d84187fcf46a..5a02bc10a2aa3 100644 --- a/src/Symfony/Component/Notifier/Bridge/Lox24/README.md +++ b/src/Symfony/Component/Notifier/Bridge/Lox24/README.md @@ -8,7 +8,7 @@ DSN example ----------- ``` -LOX24_DSN=lox24://USER:TOKEN@default?from=FROM&type=SERVICE_CODE&voice_lang=VOICE_LANGUAGE&delete_text=DELETE_TEXT&callback_data=CALLBACK_DATA +LOX24_DSN=lox24://USER:TOKEN@default?from=FROM&type=TYPE&voice_lang=VOICE_LANGUAGE&delete_text=DELETE_TEXT&callback_data=CALLBACK_DATA ``` where: diff --git a/src/Symfony/Component/Notifier/Bridge/Lox24/Tests/Lox24TransportTest.php b/src/Symfony/Component/Notifier/Bridge/Lox24/Tests/Lox24TransportTest.php index 3db43acbcdcc3..f24fc9b31c7e4 100644 --- a/src/Symfony/Component/Notifier/Bridge/Lox24/Tests/Lox24TransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Lox24/Tests/Lox24TransportTest.php @@ -9,8 +9,9 @@ * file that was distributed with this source code. */ -use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\JsonMockResponse; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\Notifier\Bridge\Lox24\Lox24Options; use Symfony\Component\Notifier\Bridge\Lox24\Lox24Transport; use Symfony\Component\Notifier\Bridge\Lox24\Type; @@ -25,7 +26,6 @@ use Symfony\Component\Notifier\Message\SmsMessage; use Symfony\Component\Notifier\Test\TransportTestCase; use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Andrei Lebedev @@ -48,13 +48,6 @@ class Lox24TransportTest extends TransportTestCase 'service_code' => 'direct', ]; - private MockObject|HttpClientInterface $client; - - protected function setUp(): void - { - $this->client = $this->createMock(HttpClientInterface::class); - } - public static function createTransport(?HttpClientInterface $client = null): Lox24Transport { return (new Lox24Transport('user', 'token', 'sender', ['type' => 'voice'], $client ?? new MockHttpClient()))->setHost('host.test'); @@ -102,7 +95,7 @@ public function testSendWithInvalidMessageType() public function testMessageFromNotEmpty() { - $this->assertRequestBody([ + $client = $this->mockHttpClient([ 'sender_id' => 'testFrom2', 'phone' => '+1411111111', 'text' => 'test text', @@ -110,14 +103,15 @@ public function testMessageFromNotEmpty() 'delivery_at' => 0, 'service_code' => 'direct', ], [], 201, ['uuid' => '123456']); - $transport = new Lox24Transport('user', 'token', 'testFrom', [], $this->client); + + $transport = new Lox24Transport('user', 'token', 'testFrom', [], $client); $message = new SmsMessage('+1411111111', 'test text', 'testFrom2'); $transport->send($message); } public function testMessageFromEmpty() { - $this->assertRequestBody([ + $client = $this->mockHttpClient([ 'sender_id' => 'testFrom', 'phone' => '+1411111111', 'text' => 'test text', @@ -125,7 +119,7 @@ public function testMessageFromEmpty() 'delivery_at' => 0, 'service_code' => 'direct', ], [], 201, ['uuid' => '123456']); - $transport = new Lox24Transport('user', 'token', 'testFrom', [], $this->client); + $transport = new Lox24Transport('user', 'token', 'testFrom', [], $client); $message = new SmsMessage('+1411111111', 'test text'); $transport->send($message); } @@ -143,7 +137,7 @@ public function testMessageFromInvalid() public function testOptionIsTextDeleted() { - $this->assertRequestBody([ + $client = $this->mockHttpClient([ 'sender_id' => 'testFrom', 'phone' => '+1411111111', 'text' => 'test text', @@ -151,7 +145,7 @@ public function testOptionIsTextDeleted() 'delivery_at' => 0, 'service_code' => 'direct', ], [], 201, ['uuid' => '123456']); - $transport = new Lox24Transport('user', 'token', 'testFrom', [], $this->client); + $transport = new Lox24Transport('user', 'token', 'testFrom', [], $client); $options = (new Lox24Options())->deleteTextAfterSending(true); $message = new SmsMessage('+1411111111', 'test text'); @@ -162,7 +156,7 @@ public function testOptionIsTextDeleted() public function testOptionDeliveryAtGreaterThanZero() { - $this->assertRequestBody([ + $client = $this->mockHttpClient([ 'sender_id' => 'testFrom', 'phone' => '+1411111111', 'text' => 'test text', @@ -170,7 +164,7 @@ public function testOptionDeliveryAtGreaterThanZero() 'delivery_at' => 1000000000, 'service_code' => 'direct', ], [], 201, ['uuid' => '123456']); - $transport = new Lox24Transport('user', 'token', 'testFrom', [], $this->client); + $transport = new Lox24Transport('user', 'token', 'testFrom', [], $client); $options = (new Lox24Options())->deliveryAt((new DateTimeImmutable())->setTimestamp(1000000000)); $message = new SmsMessage('+1411111111', 'test text'); @@ -181,7 +175,7 @@ public function testOptionDeliveryAtGreaterThanZero() public function testOptionVoiceLanguageSpanish() { - $this->assertRequestBody([ + $client = $this->mockHttpClient([ 'sender_id' => 'testFrom', 'phone' => '+1411111111', 'text' => 'test text', @@ -190,7 +184,7 @@ public function testOptionVoiceLanguageSpanish() 'service_code' => 'text2speech', 'voice_lang' => 'ES', ], [], 201, ['uuid' => '123456']); - $transport = new Lox24Transport('user', 'token', 'testFrom', [], $this->client); + $transport = new Lox24Transport('user', 'token', 'testFrom', [], $client); $options = (new Lox24Options()) ->voiceLanguage(VoiceLanguage::Spanish) @@ -203,7 +197,7 @@ public function testOptionVoiceLanguageSpanish() public function testOptionVoiceLanguageAuto() { - $this->assertRequestBody([ + $client = $this->mockHttpClient([ 'sender_id' => 'testFrom', 'phone' => '+1411111111', 'text' => 'test text', @@ -211,7 +205,7 @@ public function testOptionVoiceLanguageAuto() 'delivery_at' => 0, 'service_code' => 'text2speech', ], [], 201, ['uuid' => '123456']); - $transport = new Lox24Transport('user', 'token', 'testFrom', [], $this->client); + $transport = new Lox24Transport('user', 'token', 'testFrom', [], $client); $options = (new Lox24Options()) ->voiceLanguage(VoiceLanguage::Auto) @@ -224,7 +218,7 @@ public function testOptionVoiceLanguageAuto() public function testOptionType() { - $this->assertRequestBody([ + $client = $this->mockHttpClient([ 'sender_id' => 'testFrom', 'phone' => '+1411111111', 'text' => 'test text', @@ -233,7 +227,7 @@ public function testOptionType() 'service_code' => 'direct', ], [], 201, ['uuid' => '123456']); - $transport = new Lox24Transport('user', 'token', 'testFrom', ['type' => 'voice'], $this->client); + $transport = new Lox24Transport('user', 'token', 'testFrom', ['type' => 'voice'], $client); $options = (new Lox24Options())->type(Type::Sms); $message = new SmsMessage('+1411111111', 'test text'); @@ -244,7 +238,7 @@ public function testOptionType() public function testOptionCallbackData() { - $this->assertRequestBody([ + $client = $this->mockHttpClient([ 'sender_id' => 'testFrom', 'phone' => '+1411111111', 'text' => 'test text', @@ -254,7 +248,7 @@ public function testOptionCallbackData() 'callback_data' => 'callback_data', ], [], 201, ['uuid' => '123456']); - $transport = new Lox24Transport('user', 'token', 'testFrom', ['type' => 'voice'], $this->client); + $transport = new Lox24Transport('user', 'token', 'testFrom', ['type' => 'voice'], $client); $options = (new Lox24Options())->callbackData('callback_data'); $message = new SmsMessage('+1411111111', 'test text'); @@ -270,7 +264,7 @@ public function testResponseStatusCodeNotEqual201() 'Unable to send the SMS: "service_code: Service\'s code is invalid or unavailable.".' ); - $this->assertRequestBody([ + $client = $this->mockHttpClient([ 'sender_id' => 'testFrom', 'phone' => '+1411111111', 'text' => 'test text', @@ -294,28 +288,56 @@ public function testResponseStatusCodeNotEqual201() ], ); - $transport = new Lox24Transport('user', 'token', 'testFrom', [], $this->client); + $transport = new Lox24Transport('user', 'token', 'testFrom', [], $client); $message = new SmsMessage('+1411111111', 'test text'); $transport->send($message); } - private function assertRequestBody( + public function mockHttpClient( array $bodyOverride = [], array $headersOverride = [], int $responseStatus = 200, array $responseContent = [], - ): void { - $body = array_merge(self::REQUEST_BODY, $bodyOverride); + ): MockHttpClient { + $body = json_encode(array_merge(self::REQUEST_BODY, $bodyOverride)); $headers = array_merge(self::REQUEST_HEADERS, $headersOverride); - $response = $this->createMock(ResponseInterface::class); - $response->expects($this->once())->method('getStatusCode')->willReturn($responseStatus); - $response->expects($this->once())->method('toArray')->willReturn($responseContent); - $this->client->expects($this->once()) - ->method('request') - ->with('POST', 'https://api.lox24.eu/sms', [ - 'body' => $body, - 'headers' => $headers, - ])->willReturn($response); + + $factory = function ($method, $url, $options) use ( + $body, + $headers, + $responseStatus, + $responseContent + ): MockResponse { + $this->assertSame('POST', $method); + $this->assertSame('https://api.lox24.eu/sms', $url); + $this->assertHeaders($headers, $options['headers']); + $this->assertJsonStringEqualsJsonString($body, $options['body']); + + return new JsonMockResponse($responseContent, [ + 'http_code' => $responseStatus, + 'headers' => ['content-type' => 'application/json'], + ]); + }; + + return new MockHttpClient($factory); + } + + private function assertHeaders(array $expected, array $headers): void + { + foreach ($this->normalizeHeaders($expected) as $expectedHeader) { + $headerExists = in_array($expectedHeader, $headers, true); + $this->assertTrue($headerExists, "Header '$expectedHeader' not found in request's headers"); + } + } + + private function normalizeHeaders(array $headers): array + { + $normalized = []; + foreach ($headers as $key => $value) { + $normalized[] = $key.': '.$value; + } + + return $normalized; } } diff --git a/src/Symfony/Component/Notifier/ChatterInterface.php b/src/Symfony/Component/Notifier/ChatterInterface.php index 915190e623aaa..6d89ca921e970 100644 --- a/src/Symfony/Component/Notifier/ChatterInterface.php +++ b/src/Symfony/Component/Notifier/ChatterInterface.php @@ -14,7 +14,7 @@ use Symfony\Component\Notifier\Transport\TransportInterface; /** - * Interface for classes able to send chat messages synchronous and/or asynchronous. + * Interface for classes able to send chat messages synchronously and/or asynchronously. * * @author Fabien Potencier */ diff --git a/src/Symfony/Component/Notifier/TexterInterface.php b/src/Symfony/Component/Notifier/TexterInterface.php index e65547755cd70..a044bb6d5d835 100644 --- a/src/Symfony/Component/Notifier/TexterInterface.php +++ b/src/Symfony/Component/Notifier/TexterInterface.php @@ -14,7 +14,7 @@ use Symfony\Component\Notifier\Transport\TransportInterface; /** - * Interface for classes able to send SMS messages synchronous and/or asynchronous. + * Interface for classes able to send SMS messages synchronously and/or asynchronously. * * @author Fabien Potencier */ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 085a7b8a1cfc8..db3f897d7785f 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -443,6 +443,11 @@ public function testUnknownPseudoTypeLegacy() $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'scalar')], $this->extractor->getTypes(PseudoTypeDummy::class, 'unknownPseudoType')); } + public function testGenericInterface() + { + $this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface')); + } + /** * @group legacy * diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 0dcb0150057b9..892a7e4d30acc 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -422,7 +422,7 @@ public static function provideLegacyUnionTypes(): array ['b', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)])]], ['c', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)])]], ['d', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)], [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])])]], - ['e', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, Dummy::class, true, [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])], [new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DefaultValue::class)])])]), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], + ['e', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, Dummy::class, false, [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])], [new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, \Traversable::class, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DefaultValue::class)])])]), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], ['f', null], ['g', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)])]], ]; @@ -840,7 +840,10 @@ public static function unionTypesProvider(): iterable Type::generic( Type::object(Dummy::class), Type::array(Type::string(), Type::mixed()), - Type::union(Type::int(), Type::list(Type::generic(Type::string(), Type::object(DefaultValue::class)))), + Type::union( + Type::int(), + Type::list(Type::collection(Type::object(\Traversable::class), Type::object(DefaultValue::class))), + ), ), Type::object(ParentDummy::class), Type::null(), @@ -951,6 +954,11 @@ public static function allowPrivateAccessProvider(): array [false, Type::array(Type::string(), Type::int())], ]; } + + public function testGenericInterface() + { + $this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface')); + } } class PhpStanOmittedParamTagTypeDocBlock diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 897b9c0341a2b..0d15aa4df90dc 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -74,6 +74,7 @@ public function testGetProperties() 'noDocBlock', 'listOfStrings', 'parentAnnotation', + 'genericInterface', 'foo', 'foo2', 'foo3', @@ -139,6 +140,7 @@ public function testGetPropertiesWithCustomPrefixes() 'noDocBlock', 'listOfStrings', 'parentAnnotation', + 'genericInterface', 'foo', 'foo2', 'foo3', @@ -193,6 +195,7 @@ public function testGetPropertiesWithNoPrefixes() 'noDocBlock', 'listOfStrings', 'parentAnnotation', + 'genericInterface', 'foo', 'foo2', 'foo3', diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index 2658f6abac579..9172ad42c1131 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -167,6 +167,11 @@ class Dummy extends ParentDummy */ public $parentAnnotation; + /** + * @var \BackedEnum + */ + public $genericInterface; + public static function getStatic() { } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyUnionType.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyUnionType.php index 86ddb8a1650eb..7e2e1aa3ec8f7 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyUnionType.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyUnionType.php @@ -40,7 +40,7 @@ class DummyUnionType public $d; /** - * @var (Dummy, (int | (string)[])> | ParentDummy | null) + * @var (Dummy, (int | (\Traversable)[])> | ParentDummy | null) */ public $e; diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index a6cdb7494123c..65b53977df7cf 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -170,9 +170,9 @@ public function getType(DocType $varType): ?Type /** * Creates a {@see LegacyType} from a PHPDoc type. */ - private function createLegacyType(DocType $type, bool $nullable, ?string $docType = null): ?LegacyType + private function createLegacyType(DocType $type, bool $nullable): ?LegacyType { - $docType ??= (string) $type; + $docType = (string) $type; if ($type instanceof Collection) { $fqsen = $type->getFqsen(); @@ -183,10 +183,17 @@ private function createLegacyType(DocType $type, bool $nullable, ?string $docTyp [$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen); + $collection = \is_a($class, \Traversable::class, true) || \is_a($class, \ArrayAccess::class, true); + + // it's safer to fall back to other extractors if the generic type is too abstract + if (!$collection && !class_exists($class)) { + return null; + } + $keys = $this->getTypes($type->getKeyType()); $values = $this->getTypes($type->getValueType()); - return new LegacyType($phpType, $nullable, $class, true, $keys, $values); + return new LegacyType($phpType, $nullable, $class, $collection, $keys, $values); } // Cannot guess diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php index d9b1a7a3ccd31..ac37d144ef74d 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php @@ -125,6 +125,13 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array return [$mainType]; } + $collection = $mainType->isCollection() || \in_array($mainType->getClassName(), [\Traversable::class, \Iterator::class, \IteratorAggregate::class, \ArrayAccess::class, \Generator::class], true); + + // it's safer to fall back to other extractors if the generic type is too abstract + if (!$collection && !class_exists($mainType->getClassName())) { + return []; + } + $collectionKeyTypes = $mainType->getCollectionKeyTypes(); $collectionKeyValues = []; if (1 === \count($node->genericTypes)) { @@ -140,7 +147,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array } } - return [new Type($mainType->getBuiltinType(), $mainType->isNullable(), $mainType->getClassName(), true, $collectionKeyTypes, $collectionKeyValues)]; + return [new Type($mainType->getBuiltinType(), $mainType->isNullable(), $mainType->getClassName(), $collection, $collectionKeyTypes, $collectionKeyValues)]; } if ($node instanceof ArrayShapeNode) { return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]; diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf index 6d7dc7fc23e33..93ff24f330735 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf @@ -64,7 +64,7 @@ Too many failed login attempts, please try again later. - Massa intents d'inici de sessió fallits, torneu-ho a provar més tard. + Massa intents d'inici de sessió fallits, si us plau torneu-ho a provar més tard. Invalid or expired login link. @@ -72,11 +72,11 @@ Too many failed login attempts, please try again in %minutes% minute. - Massa intents d'inici de sessió fallits, torneu-ho a provar en %minutes% minut. + Massa intents d'inici de sessió fallits, si us plau torneu-ho a provar en %minutes% minut. Too many failed login attempts, please try again in %minutes% minutes. - Massa intents fallits d'inici de sessió, torneu-ho a provar d'aquí a %minutes% minuts. + Massa intents d'inici de sessió fallits, si us plau torneu-ho a provar en %minutes% minuts. diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index 4cb990934a549..7109ff244a79f 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -18,6 +18,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; @@ -130,7 +131,7 @@ private function getCredentials(Request $request): array $credentials['username'] = trim($credentials['username']); if ('' === $credentials['username']) { - throw new BadRequestHttpException(sprintf('The key "%s" must be a non-empty string.', $this->options['username_parameter'])); + throw new BadCredentialsException(sprintf('The key "%s" must be a non-empty string.', $this->options['username_parameter'])); } $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $credentials['username']); @@ -140,7 +141,7 @@ private function getCredentials(Request $request): array } if ('' === (string) $credentials['password']) { - throw new BadRequestHttpException(sprintf('The key "%s" must be a non-empty string.', $this->options['password_parameter'])); + throw new BadCredentialsException(sprintf('The key "%s" must be a non-empty string.', $this->options['password_parameter'])); } if (!\is_string($credentials['csrf_token'] ?? '') && (!\is_object($credentials['csrf_token']) || !method_exists($credentials['csrf_token'], '__toString'))) { diff --git a/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php b/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php index 269c37709b547..3e05c71dbbcd5 100644 --- a/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php @@ -46,7 +46,7 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo foreach ($attributes as $attribute) { $id = $this->getTokenId($attribute->id, $request, $arguments); - if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($id, $request->request->getString($attribute->tokenKey)))) { + if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($id, $request->getPayload()->getString($attribute->tokenKey)))) { throw new InvalidCsrfTokenException('Invalid CSRF token.'); } } diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index 1b3ff9d7aa989..4457a8dc5f9b3 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -87,7 +87,7 @@ public function authenticate(RequestEvent $event): void } $request = $event->getRequest(); - $session = $request->hasPreviousSession() ? $request->getSession() : null; + $session = !$request->attributes->getBoolean('_stateless') && $request->hasPreviousSession() ? $request->getSession() : null; $request->attributes->set('_security_firewall_run', $this->sessionKey); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php index e58f5020e3a7a..9469eab7c2a94 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php @@ -44,7 +44,7 @@ protected function setUp(): void public function testHandleWhenUsernameEmpty() { - $this->expectException(BadRequestHttpException::class); + $this->expectException(BadCredentialsException::class); $this->expectExceptionMessage('The key "_username" must be a non-empty string.'); $request = Request::create('/login_check', 'POST', ['_username' => '', '_password' => 's$cr$t']); @@ -56,7 +56,7 @@ public function testHandleWhenUsernameEmpty() public function testHandleWhenPasswordEmpty() { - $this->expectException(BadRequestHttpException::class); + $this->expectException(BadCredentialsException::class); $this->expectExceptionMessage('The key "_password" must be a non-empty string.'); $request = Request::create('/login_check', 'POST', ['_username' => 'foo', '_password' => '']); diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php index cbbdc3b15fe62..00d464a6c69da 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php @@ -88,6 +88,28 @@ public function testIsCsrfTokenValidCalledCorrectly() $listener->onKernelControllerArguments($event); } + public function testIsCsrfTokenValidCalledCorrectlyInPayload() + { + $request = new Request(server: ['headers' => ['content-type' => 'application/json']], content: json_encode(['_token' => 'bar'])); + + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->once()) + ->method('isTokenValid') + ->with(new CsrfToken('foo', 'bar')) + ->willReturn(true); + + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + [new IsCsrfTokenValidAttributeMethodsController(), 'withDefaultTokenKey'], + [], + $request, + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } + public function testIsCsrfTokenValidCalledCorrectlyWithCustomExpressionId() { $request = new Request(query: ['id' => '123'], request: ['_token' => 'bar']); diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index df3d693f21fc9..3275d976a106b 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -566,12 +566,30 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass return (float) $data; } + if (LegacyType::BUILTIN_TYPE_BOOL === $builtinType && \is_string($data) && ($context[self::FILTER_BOOL] ?? false)) { + return filter_var($data, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE); + } + if ((LegacyType::BUILTIN_TYPE_FALSE === $builtinType && false === $data) || (LegacyType::BUILTIN_TYPE_TRUE === $builtinType && true === $data)) { return $data; } - if (('is_'.$builtinType)($data)) { - return $data; + switch ($builtinType) { + case LegacyType::BUILTIN_TYPE_ARRAY: + case LegacyType::BUILTIN_TYPE_BOOL: + case LegacyType::BUILTIN_TYPE_CALLABLE: + case LegacyType::BUILTIN_TYPE_FLOAT: + case LegacyType::BUILTIN_TYPE_INT: + case LegacyType::BUILTIN_TYPE_ITERABLE: + case LegacyType::BUILTIN_TYPE_NULL: + case LegacyType::BUILTIN_TYPE_OBJECT: + case LegacyType::BUILTIN_TYPE_RESOURCE: + case LegacyType::BUILTIN_TYPE_STRING: + if (('is_'.$builtinType)($data)) { + return $data; + } + + break; } } catch (NotNormalizableValueException|InvalidArgumentException $e) { if (!$isUnionType && !$isNullable) { @@ -626,8 +644,11 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass private function validateAndDenormalize(Type $type, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed { $expectedTypes = []; + $isUnionType = $type->asNonNullable() instanceof UnionType; + $e = null; $extraAttributesException = null; $missingConstructorArgumentsException = null; + $isNullable = false; $types = match (true) { $type instanceof IntersectionType => throw new LogicException('Unable to handle intersection type.'), @@ -665,12 +686,19 @@ private function validateAndDenormalize(Type $type, string $currentClass, string // That's why we have to transform the values, if one of these non-string basic datatypes is expected. $typeIdentifier = $t->getTypeIdentifier(); if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) { + if ('' === $data) { + if (TypeIdentifier::ARRAY === $typeIdentifier) { + return []; + } + + if (TypeIdentifier::STRING === $typeIdentifier) { + return ''; + } + + $isNullable = $isNullable ?: $type->isNullable(); + } + switch ($typeIdentifier) { - case TypeIdentifier::ARRAY: - if ('' === $data) { - return []; - } - break; case TypeIdentifier::BOOL: // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1" if ('false' === $data || '0' === $data) { @@ -773,25 +801,42 @@ private function validateAndDenormalize(Type $type, string $currentClass, string return (float) $data; } - if ((TypeIdentifier::FALSE === $typeIdentifier && false === $data) || (TypeIdentifier::TRUE === $typeIdentifier && true === $data)) { - return $data; + if (TypeIdentifier::BOOL === $typeIdentifier && \is_string($data) && ($context[self::FILTER_BOOL] ?? false)) { + return filter_var($data, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE); } - if (('is_'.$typeIdentifier->value)($data)) { + $dataMatchesExpectedType = match ($typeIdentifier) { + TypeIdentifier::ARRAY => \is_array($data), + TypeIdentifier::BOOL => \is_bool($data), + TypeIdentifier::CALLABLE => \is_callable($data), + TypeIdentifier::FALSE => false === $data, + TypeIdentifier::FLOAT => \is_float($data), + TypeIdentifier::INT => \is_int($data), + TypeIdentifier::ITERABLE => is_iterable($data), + TypeIdentifier::MIXED => true, + TypeIdentifier::NULL => null === $data, + TypeIdentifier::OBJECT => \is_object($data), + TypeIdentifier::RESOURCE => \is_resource($data), + TypeIdentifier::STRING => \is_string($data), + TypeIdentifier::TRUE => true === $data, + default => false, + }; + + if ($dataMatchesExpectedType) { return $data; } } catch (NotNormalizableValueException|InvalidArgumentException $e) { - if (!$type instanceof UnionType) { + if (!$isUnionType && !$isNullable) { throw $e; } } catch (ExtraAttributesException $e) { - if (!$type instanceof UnionType) { + if (!$isUnionType && !$isNullable) { throw $e; } $extraAttributesException ??= $e; } catch (MissingConstructorArgumentsException $e) { - if (!$type instanceof UnionType) { + if (!$isUnionType && !$isNullable) { throw $e; } @@ -811,6 +856,10 @@ private function validateAndDenormalize(Type $type, string $currentClass, string throw $missingConstructorArgumentsException; } + if (!$isUnionType && $e) { + throw $e; + } + if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { return $data; } diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index 1b51b729c660b..1f19bd9d86533 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -177,15 +177,19 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string if ($context['_read_attributes'] ?? true) { if (!isset(self::$isReadableCache[$class.$attribute])) { - self::$isReadableCache[$class.$attribute] = $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute); + self::$isReadableCache[$class.$attribute] = (\is_object($classOrObject) && $this->propertyAccessor->isReadable($classOrObject, $attribute)) || $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute); } return self::$isReadableCache[$class.$attribute]; } if (!isset(self::$isWritableCache[$class.$attribute])) { - self::$isWritableCache[$class.$attribute] = $this->propertyInfoExtractor->isWritable($class, $attribute) - || (($writeInfo = $this->writeInfoExtractor->getWriteInfo($class, $attribute)) && PropertyWriteInfo::TYPE_NONE !== $writeInfo->getType()); + if (str_contains($attribute, '.')) { + self::$isWritableCache[$class.$attribute] = true; + } else { + self::$isWritableCache[$class.$attribute] = $this->propertyInfoExtractor->isWritable($class, $attribute) + || (($writeInfo = $this->writeInfoExtractor->getWriteInfo($class, $attribute)) && PropertyWriteInfo::TYPE_NONE !== $writeInfo->getType()); + } } return self::$isWritableCache[$class.$attribute]; diff --git a/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php b/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php index 8da1b471bd567..57f2b568ef44e 100644 --- a/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php +++ b/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php @@ -156,7 +156,7 @@ class ZooWithKeyTypes public $animalsString = []; /** @var array */ public $animalsUnion = []; - /** @var \stdClass */ + /** @var \Traversable */ public $animalsGenerics = []; } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php index 41da0eac8a999..ef7a8c906dc51 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php @@ -26,6 +26,6 @@ public function __construct() public function denormalize(DenormalizerInterface $denormalizer, $data, ?string $format = null, array $context = []): void { - throw new NotNormalizableValueException(); + throw new NotNormalizableValueException('Custom exception message'); } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/property-path-mapping.yaml b/src/Symfony/Component/Serializer/Tests/Fixtures/property-path-mapping.yaml new file mode 100644 index 0000000000000..834b39150fe89 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/property-path-mapping.yaml @@ -0,0 +1,5 @@ +Symfony\Component\Serializer\Tests\Normalizer\ObjectOuter: + attributes: + inner.foo: + serialized_name: inner_foo + groups: [ 'read' ] diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index f41c0fdf3956b..e1b1031dabe39 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -946,6 +946,7 @@ public function testDenormalizeUntypedFormat() public function testDenormalizeUntypedFormatNotNormalizable() { $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('Custom exception message'); $serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); $serializer->denormalize(['value' => 'test'], DummyWithNotNormalizable::class, 'xml'); } @@ -1132,7 +1133,7 @@ public function testNormalizationWithMaxDepthOnStdclassObjectDoesNotThrowWarning public function testDenormalizeCollectionOfScalarTypesPropertyWithPhpDocExtractor() { - $normalizer = new AbstractObjectNormalizerWithMetadataAndPhpDocExtractor(); + $normalizer = new AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors(); $data = [ 'type' => 'foo', 'values' => [ @@ -1150,7 +1151,7 @@ public function testDenormalizeCollectionOfScalarTypesPropertyWithPhpDocExtracto public function testDenormalizeCollectionOfUnionTypesPropertyWithPhpDocExtractor() { - $normalizer = new AbstractObjectNormalizerWithMetadataAndPhpDocExtractor(); + $normalizer = new AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors(); $data = [ 'values1' => [ 'foo' => 'foo', @@ -1166,6 +1167,63 @@ public function testDenormalizeCollectionOfUnionTypesPropertyWithPhpDocExtractor $this->assertEquals($expected, $normalizer->denormalize($data, UnionCollectionDocBlockDummy::class)); } + + public function testDenormalizeMixedProperty() + { + $normalizer = new AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors(); + $expected = new MixedPropertyDummy(); + $expected->foo = 'bar'; + + $this->assertEquals($expected, $normalizer->denormalize(['foo' => 'bar'], MixedPropertyDummy::class)); + } + + /** + * @dataProvider provideBooleanTypesData + */ + public function testDenormalizeBooleanTypesWithNotMatchingData(array $data, string $type) + { + $normalizer = new AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors(); + + $this->expectException(NotNormalizableValueException::class); + + $normalizer->denormalize($data, $type); + } + + public function provideBooleanTypesData() + { + return [ + [['foo' => true], FalsePropertyDummy::class], + [['foo' => false], TruePropertyDummy::class], + ]; + } + + /** + * @dataProvider provideDenormalizeWithFilterBoolData + */ + public function testDenormalizeBooleanTypeWithFilterBool(array $data, ?bool $expectedFoo) + { + $normalizer = new AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors(); + + $dummy = $normalizer->denormalize($data, BoolPropertyDummy::class, null, [AbstractNormalizer::FILTER_BOOL => true]); + + $this->assertSame($expectedFoo, $dummy->foo); + } + + public function provideDenormalizeWithFilterBoolData(): array + { + return [ + [['foo' => 'true'], true], + [['foo' => '1'], true], + [['foo' => 'yes'], true], + [['foo' => 'false'], false], + [['foo' => '0'], false], + [['foo' => 'no'], false], + [['foo' => ''], false], + [['foo' => null], null], + [['foo' => 'null'], null], + [['foo' => 'something'], null], + ]; + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer @@ -1268,6 +1326,11 @@ class SnakeCaseNestedDummy public $fooBar; } +class MixedPropertyDummy +{ + public mixed $foo; +} + #[DiscriminatorMap(typeProperty: 'type', mapping: [ 'first' => FirstNestedDummyWithConstructorAndDiscriminator::class, 'second' => SecondNestedDummyWithConstructorAndDiscriminator::class, @@ -1434,6 +1497,24 @@ class XmlScalarDummy public $value; } +class FalsePropertyDummy +{ + /** @var false */ + public $foo; +} + +class TruePropertyDummy +{ + /** @var true */ + public $foo; +} + +class BoolPropertyDummy +{ + /** @var null|bool */ + public $foo; +} + class SerializerCollectionDummy implements SerializerInterface, DenormalizerInterface { private array $normalizers; @@ -1612,11 +1693,11 @@ public function __construct( public array $values2; } -class AbstractObjectNormalizerWithMetadataAndPhpDocExtractor extends AbstractObjectNormalizer +class AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors extends AbstractObjectNormalizer { public function __construct() { - parent::__construct(new ClassMetadataFactory(new AttributeLoader()), null, new PropertyInfoExtractor([], [new PhpDocExtractor()])); + parent::__construct(new ClassMetadataFactory(new AttributeLoader()), null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()])); } protected function extractAttributes(object $object, ?string $format = null, array $context = []): array diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 19bbcde2c710e..c5f86939e02d2 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -27,6 +27,7 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; @@ -918,6 +919,40 @@ public function testDenormalizeWithIgnoreAttributeAndPrivateProperties() $this->assertEquals($expected, $obj); } + + public function testNormalizeWithPropertyPath() + { + $classMetadataFactory = new ClassMetadataFactory(new YamlFileLoader(__DIR__.'/../Fixtures/property-path-mapping.yaml')); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $dummyInner = new ObjectInner(); + $dummyInner->foo = 'foo'; + $dummy = new ObjectOuter(); + $dummy->setInner($dummyInner); + + $this->assertSame(['inner_foo' => 'foo'], $normalizer->normalize($dummy, 'json', ['groups' => 'read'])); + } + + public function testDenormalizeWithPropertyPath() + { + $classMetadataFactory = new ClassMetadataFactory(new YamlFileLoader(__DIR__.'/../Fixtures/property-path-mapping.yaml')); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $dummy = new ObjectOuter(); + $dummy->setInner(new ObjectInner()); + + $obj = $normalizer->denormalize(['inner_foo' => 'foo'], ObjectOuter::class, 'json', [ + 'object_to_populate' => $dummy, + 'groups' => 'read', + ]); + + $expectedInner = new ObjectInner(); + $expectedInner->foo = 'foo'; + $expected = new ObjectOuter(); + $expected->setInner($expectedInner); + + $this->assertEquals($expected, $obj); + } } class ProxyObjectDummy extends ObjectDummy diff --git a/src/Symfony/Component/String/AbstractUnicodeString.php b/src/Symfony/Component/String/AbstractUnicodeString.php index f3acb2e1d68e7..f3ffcf09c34e8 100644 --- a/src/Symfony/Component/String/AbstractUnicodeString.php +++ b/src/Symfony/Component/String/AbstractUnicodeString.php @@ -361,8 +361,8 @@ public function reverse(): static public function snake(): static { - $str = $this->camel(); - $str->string = mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str->string), 'UTF-8'); + $str = clone $this; + $str->string = str_replace(' ', '_', mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1 \2', $str->string), 'UTF-8')); return $str; } diff --git a/src/Symfony/Component/String/ByteString.php b/src/Symfony/Component/String/ByteString.php index e6b56ae11f316..6e23a6f9155ca 100644 --- a/src/Symfony/Component/String/ByteString.php +++ b/src/Symfony/Component/String/ByteString.php @@ -347,8 +347,8 @@ public function slice(int $start = 0, ?int $length = null): static public function snake(): static { - $str = $this->camel(); - $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string)); + $str = clone $this; + $str->string = str_replace(' ', '_', strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1 \2', $str->string))); return $str; } diff --git a/src/Symfony/Component/String/Inflector/EnglishInflector.php b/src/Symfony/Component/String/Inflector/EnglishInflector.php index 56f03b7fd6aeb..77ebc134a436f 100644 --- a/src/Symfony/Component/String/Inflector/EnglishInflector.php +++ b/src/Symfony/Component/String/Inflector/EnglishInflector.php @@ -25,8 +25,32 @@ final class EnglishInflector implements InflectorInterface // Fourth entry: Whether the suffix may succeed a consonant // Fifth entry: singular suffix, normal - // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) - ['a', 1, true, true, ['on', 'um']], + // bacteria (bacterium) + ['airetcab', 8, true, true, 'bacterium'], + + // corpora (corpus) + ['aroproc', 7, true, true, 'corpus'], + + // criteria (criterion) + ['airetirc', 8, true, true, 'criterion'], + + // curricula (curriculum) + ['alucirruc', 9, true, true, 'curriculum'], + + // genera (genus) + ['areneg', 6, true, true, 'genus'], + + // media (medium) + ['aidem', 5, true, true, 'medium'], + + // memoranda (memorandum) + ['adnaromem', 9, true, true, 'memorandum'], + + // phenomena (phenomenon) + ['anemonehp', 9, true, true, 'phenomenon'], + + // strata (stratum) + ['atarts', 6, true, true, 'stratum'], // nebulae (nebula) ['ea', 2, true, true, 'a'], @@ -141,7 +165,7 @@ final class EnglishInflector implements InflectorInterface // shoes (shoe) ['se', 2, true, true, ['', 'e']], - // status (status) + // status (status) ['sutats', 6, true, true, 'status'], // tags (tag) @@ -241,7 +265,7 @@ final class EnglishInflector implements InflectorInterface // albums (album) ['mubla', 5, true, true, 'albums'], - // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) + // bacteria (bacterium), curricula (curriculum), media (medium), memoranda (memorandum), phenomena (phenomenon), strata (stratum) ['mu', 2, true, true, 'a'], // men (man), women (woman) @@ -250,20 +274,11 @@ final class EnglishInflector implements InflectorInterface // people (person) ['nosrep', 6, true, true, ['persons', 'people']], - // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) - ['noi', 3, true, true, 'ions'], - - // coupon (coupons) - ['nop', 3, true, true, 'pons'], + // criteria (criterion) + ['noiretirc', 9, true, true, 'criteria'], - // seasons (season), treasons (treason), poisons (poison), lessons (lesson) - ['nos', 3, true, true, 'sons'], - - // icons (icon) - ['noc', 3, true, true, 'cons'], - - // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) - ['no', 2, true, true, 'a'], + // phenomena (phenomenon) + ['nonemonehp', 10, true, true, 'phenomena'], // echoes (echo) ['ohce', 4, true, true, 'echoes'], @@ -274,6 +289,9 @@ final class EnglishInflector implements InflectorInterface // atlases (atlas) ['salta', 5, true, true, 'atlases'], + // aliases (alias) + ['saila', 5, true, true, 'aliases'], + // irises (iris) ['siri', 4, true, true, 'irises'], diff --git a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php index eabe7b912bed5..f8b0509ffa185 100644 --- a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php @@ -1075,6 +1075,8 @@ public static function provideSnake() ['symfony_is_great', 'symfonyIsGREAT'], ['symfony_is_really_great', 'symfonyIsREALLYGreat'], ['symfony', 'SYMFONY'], + ['symfony_is_great', 'SYMFONY IS GREAT'], + ['symfony_is_great', 'SYMFONY_IS_GREAT'], ]; } diff --git a/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php b/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php index 89f4966a40c1f..fb5d04300305a 100644 --- a/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php +++ b/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php @@ -35,7 +35,7 @@ public static function singularizeProvider() ['atlases', ['atlas', 'atlase', 'atlasis']], ['axes', ['ax', 'axe', 'axis']], ['babies', 'baby'], - ['bacteria', ['bacterion', 'bacterium']], + ['bacteria', 'bacterium'], ['bases', ['bas', 'base', 'basis']], ['batches', ['batch', 'batche']], ['beaux', 'beau'], @@ -46,6 +46,7 @@ public static function singularizeProvider() ['bureaux', 'bureau'], ['buses', ['bus', 'buse', 'busis']], ['bushes', ['bush', 'bushe']], + ['buttons', 'button'], ['calves', ['calf', 'calve', 'calff']], ['cars', 'car'], ['cassettes', ['cassett', 'cassette']], @@ -57,10 +58,12 @@ public static function singularizeProvider() ['cliffs', 'cliff'], ['codes', 'code'], ['committee', 'committee'], + ['corpora', 'corpus'], + ['coupons', 'coupon'], ['crises', ['cris', 'crise', 'crisis']], - ['criteria', ['criterion', 'criterium']], + ['criteria', 'criterion'], ['cups', 'cup'], - ['coupons', 'coupon'], + ['curricula', 'curriculum'], ['data', 'data'], ['days', 'day'], ['discos', 'disco'], @@ -86,6 +89,7 @@ public static function singularizeProvider() ['funguses', ['fungus', 'funguse', 'fungusis']], ['garages', ['garag', 'garage']], ['geese', 'goose'], + ['genera', 'genus'], ['halves', ['half', 'halve', 'halff']], ['hats', 'hat'], ['heroes', ['hero', 'heroe']], @@ -106,6 +110,8 @@ public static function singularizeProvider() ['lives', 'life'], ['matrices', ['matrex', 'matrix', 'matrice']], ['matrixes', 'matrix'], + ['media', 'medium'], + ['memoranda', 'memorandum'], ['men', 'man'], ['mice', 'mouse'], ['moves', 'move'], @@ -120,7 +126,7 @@ public static function singularizeProvider() ['parties', 'party'], ['people', 'person'], ['persons', 'person'], - ['phenomena', ['phenomenon', 'phenomenum']], + ['phenomena', 'phenomenon'], ['photos', 'photo'], ['pianos', 'piano'], ['plateaux', 'plateau'], @@ -146,7 +152,7 @@ public static function singularizeProvider() ['status', 'status'], ['statuses', 'status'], ['stories', 'story'], - ['strata', ['straton', 'stratum']], + ['strata', 'stratum'], ['suitcases', ['suitcas', 'suitcase', 'suitcasis']], ['syllabi', 'syllabus'], ['tags', 'tag'], @@ -200,7 +206,9 @@ public static function pluralizeProvider() ['bureau', ['bureaus', 'bureaux']], ['bus', 'buses'], ['bush', 'bushes'], + ['button', 'buttons'], ['calf', ['calfs', 'calves']], + ['campus', 'campuses'], ['car', 'cars'], ['cassette', 'cassettes'], ['cave', 'caves'], @@ -210,10 +218,11 @@ public static function pluralizeProvider() ['circus', 'circuses'], ['cliff', 'cliffs'], ['committee', 'committees'], + ['coupon', 'coupons'], ['crisis', 'crises'], - ['criteria', 'criterion'], + ['criterion', 'criteria'], ['cup', 'cups'], - ['coupon', 'coupons'], + ['curriculum', 'curricula'], ['data', 'data'], ['day', 'days'], ['disco', 'discos'], @@ -237,10 +246,12 @@ public static function pluralizeProvider() ['half', ['halfs', 'halves']], ['hat', 'hats'], ['hero', 'heroes'], + ['hippocampus', 'hippocampi'], ['hippopotamus', 'hippopotami'], // hippopotamuses ['hoax', 'hoaxes'], ['hoof', ['hoofs', 'hooves']], ['house', 'houses'], + ['icon', 'icons'], ['index', ['indicies', 'indexes']], ['ion', 'ions'], ['iris', 'irises'], @@ -253,6 +264,8 @@ public static function pluralizeProvider() ['louse', 'lice'], ['man', 'men'], ['matrix', ['matricies', 'matrixes']], + ['medium', 'media'], + ['memorandum', 'memoranda'], ['mouse', 'mice'], ['move', 'moves'], ['movie', 'movies'], @@ -286,6 +299,7 @@ public static function pluralizeProvider() ['shoe', 'shoes'], ['species', 'species'], ['status', ['status', 'statuses']], + ['stratum', 'strata'], ['spy', 'spies'], ['staff', 'staves'], ['story', 'stories'], @@ -303,6 +317,7 @@ public static function pluralizeProvider() ['hippocampus', 'hippocampi'], ['campus', 'campuses'], ['hardware', 'hardware'], + ['alias', 'aliases'], // test casing: if the first letter was uppercase, it should remain so ['Man', 'Men'], diff --git a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php index e62c94179ecb4..9e96e755c1288 100644 --- a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php @@ -48,7 +48,7 @@ public function validate(mixed $value, Constraint $constraint): void if (\in_array($element, $collectionElements, true)) { $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameter('{{ value }}', $this->formatValue($element)) ->setCode(Unique::IS_NOT_UNIQUE) ->addViolation(); diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf index 3d0b819a95441..3c0ace5490efd 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ro.xlf @@ -440,7 +440,7 @@ This URL is missing a top-level domain. - Acest URL îi lipsește un domeniu de nivel superior. + Acestui URL îi lipsește un domeniu de nivel superior. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php index 1f509130eb01e..6322183fd5ebf 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php @@ -403,7 +403,7 @@ public static function getInvalidHSL(): array } /** - * @dataProvider getInvalidHSL + * @dataProvider getInvalidHSLA */ public function testInvalidHSLA($cssColor) { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php index 3c2dd9f21c98f..0d5ccdb8e56e2 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php @@ -61,7 +61,7 @@ public static function getValidValues() /** * @dataProvider getInvalidValues */ - public function testInvalidValues($value) + public function testInvalidValues($value, $expectedMessageParam) { $constraint = new Unique([ 'message' => 'myMessage', @@ -69,7 +69,7 @@ public function testInvalidValues($value) $this->validator->validate($value, $constraint); $this->buildViolation('myMessage') - ->setParameter('{{ value }}', 'array') + ->setParameter('{{ value }}', $expectedMessageParam) ->setCode(Unique::IS_NOT_UNIQUE) ->assertRaised(); } @@ -79,12 +79,12 @@ public static function getInvalidValues() $object = new \stdClass(); return [ - yield 'not unique booleans' => [[true, true]], - yield 'not unique integers' => [[1, 2, 3, 3]], - yield 'not unique floats' => [[0.1, 0.2, 0.1]], - yield 'not unique string' => [['a', 'b', 'a']], - yield 'not unique arrays' => [[[1, 1], [2, 3], [1, 1]]], - yield 'not unique objects' => [[$object, $object]], + yield 'not unique booleans' => [[true, true], 'true'], + yield 'not unique integers' => [[1, 2, 3, 3], 3], + yield 'not unique floats' => [[0.1, 0.2, 0.1], 0.1], + yield 'not unique string' => [['a', 'b', 'a'], '"a"'], + yield 'not unique arrays' => [[[1, 1], [2, 3], [1, 1]], 'array'], + yield 'not unique objects' => [[$object, $object], 'object'], ]; } @@ -94,7 +94,7 @@ public function testInvalidValueNamed() $this->validator->validate([1, 2, 3, 3], $constraint); $this->buildViolation('myMessage') - ->setParameter('{{ value }}', 'array') + ->setParameter('{{ value }}', '3') ->setCode(Unique::IS_NOT_UNIQUE) ->assertRaised(); } @@ -174,7 +174,7 @@ public function testExpectsInvalidNonStrictComparison() ])); $this->buildViolation('myMessage') - ->setParameter('{{ value }}', 'array') + ->setParameter('{{ value }}', '1') ->setCode(Unique::IS_NOT_UNIQUE) ->assertRaised(); } @@ -200,7 +200,7 @@ public function testExpectsInvalidCaseInsensitiveComparison() ])); $this->buildViolation('myMessage') - ->setParameter('{{ value }}', 'array') + ->setParameter('{{ value }}', '"hello"') ->setCode(Unique::IS_NOT_UNIQUE) ->assertRaised(); } @@ -246,14 +246,14 @@ public static function getInvalidFieldNames(): array /** * @dataProvider getInvalidCollectionValues */ - public function testInvalidCollectionValues(array $value, array $fields) + public function testInvalidCollectionValues(array $value, array $fields, string $expectedMessageParam) { $this->validator->validate($value, new Unique([ 'message' => 'myMessage', ], fields: $fields)); $this->buildViolation('myMessage') - ->setParameter('{{ value }}', 'array') + ->setParameter('{{ value }}', $expectedMessageParam) ->setCode(Unique::IS_NOT_UNIQUE) ->assertRaised(); } @@ -264,23 +264,25 @@ public static function getInvalidCollectionValues(): array 'unique string' => [[ ['lang' => 'eng', 'translation' => 'hi'], ['lang' => 'eng', 'translation' => 'hello'], - ], ['lang']], + ], ['lang'], 'array'], 'unique floats' => [[ ['latitude' => 51.509865, 'longitude' => -0.118092, 'poi' => 'capital'], ['latitude' => 52.520008, 'longitude' => 13.404954], ['latitude' => 51.509865, 'longitude' => -0.118092], - ], ['latitude', 'longitude']], + ], ['latitude', 'longitude'], 'array'], 'unique int' => [[ ['id' => 1, 'email' => 'bar@email.com'], ['id' => 1, 'email' => 'foo@email.com'], - ], ['id']], + ], ['id'], 'array'], 'unique null' => [ [null, null], [], + 'null', ], 'unique field null' => [ [['nullField' => null], ['nullField' => null]], ['nullField'], + 'array', ], ]; } diff --git a/src/Symfony/Component/VarDumper/Caster/FFICaster.php b/src/Symfony/Component/VarDumper/Caster/FFICaster.php index 8c3ad02bbcb38..a9d2abc1b9def 100644 --- a/src/Symfony/Component/VarDumper/Caster/FFICaster.php +++ b/src/Symfony/Component/VarDumper/Caster/FFICaster.php @@ -115,11 +115,21 @@ private static function castFFIPointer(Stub $stub, CType $type, ?CData $data = n private static function castFFIStringValue(CData $data): string|CutStub { $result = []; + $ffi = \FFI::cdef(<<zend_get_page_size(); + + // get cdata address + $start = $ffi->cast('uintptr_t', $ffi->cast('char*', $data))->cdata; + // accessing memory in the same page as $start is safe + $max = min(self::MAX_STRING_LENGTH, ($start | ($pageSize - 1)) - $start); + + for ($i = 0; $i < $max; ++$i) { $result[$i] = $data[$i]; - if ("\0" === $result[$i]) { + if ("\0" === $data[$i]) { return implode('', $result); } } diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/DOMCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/DOMCasterTest.php index 817acd3e0ef10..1f952f6f9bb15 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/DOMCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/DOMCasterTest.php @@ -227,7 +227,7 @@ public function testCastModernElement() $attr = \Dom\HTMLDocument::createEmpty()->createElement('foo'); $this->assertDumpMatchesFormat(<<<'EODUMP' - Dom\Element {%A + Dom\HTMLElement {%A +tagName: ? string %A} EODUMP, diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php index 5e7ec147bbbe6..362e0a2c532c9 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php @@ -24,6 +24,11 @@ class FFICasterTest extends TestCase { use VarDumperTestTrait; + /** + * @see FFICaster::MAX_STRING_LENGTH + */ + private const MAX_STRING_LENGTH = 255; + protected function setUp(): void { if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && 'preload' === \ini_get('ffi.enable')) { @@ -173,17 +178,24 @@ public function testCastCuttedPointerToChar() { $actualMessage = str_repeat('Hello World!', 30)."\0"; $actualLength = \strlen($actualMessage); - - $expectedMessage = 'Hello World!Hello World!Hello World!Hello World!' - .'Hello World!Hello World!Hello World!Hello World!Hello World!Hel' - .'lo World!Hello World!Hello World!Hello World!Hello World!Hello ' - .'World!Hello World!Hello World!Hello World!Hello World!Hello Wor' - .'ld!Hello World!Hel'; + $expectedMessage = substr($actualMessage, 0, self::MAX_STRING_LENGTH); $string = \FFI::cdef()->new('char['.$actualLength.']'); $pointer = \FFI::addr($string[0]); \FFI::memcpy($pointer, $actualMessage, $actualLength); + // the max length is platform-dependent and can be less than 255, + // so we need to cut the expected message to the maximum length + // allowed by pages size of the current system + $ffi = \FFI::cdef(<<zend_get_page_size(); + $start = $ffi->cast('uintptr_t', $ffi->cast('char*', $pointer))->cdata; + $max = min(self::MAX_STRING_LENGTH, ($start | ($pageSize - 1)) - $start); + $expectedMessage = substr($expectedMessage, 0, $max); + $this->assertDumpEquals(<< size 8 align 8 { cdata: "$expectedMessage"… @@ -191,34 +203,21 @@ public function testCastCuttedPointerToChar() PHP, $pointer); } - /** - * It is worth noting that such a test can cause SIGSEGV, as it breaks - * into "foreign" memory. However, this is only theoretical, since - * memory is allocated within the PHP process and almost always "garbage - * data" will be read from the PHP process itself. - * - * If this test fails for some reason, please report it: We may have to - * disable the dumping of strings ("char*") feature in VarDumper. - * - * @see FFICaster::castFFIStringValue() - */ public function testCastNonTrailingCharPointer() { $actualMessage = 'Hello World!'; $actualLength = \strlen($actualMessage); - $string = \FFI::cdef()->new('char['.$actualLength.']'); + $string = \FFI::cdef()->new('char['.($actualLength + 1).']'); $pointer = \FFI::addr($string[0]); - \FFI::memcpy($pointer, $actualMessage, $actualLength); - // Remove automatically addition of the trailing "\0" and remove trailing "\0" $pointer = \FFI::cdef()->cast('char*', \FFI::cdef()->cast('void*', $pointer)); $pointer[$actualLength] = "\x01"; $this->assertDumpMatchesFormat(<< size 8 align 8 { - cdata: "$actualMessage%s" + cdata: %A"$actualMessage%s" } PHP, $pointer); } diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index c5978ee06820f..4cf0f65b4531e 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -197,10 +197,35 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf $body = $methods ? "\n".implode("\n\n", $methods)."\n" : ''; $propertyScopes = $class ? self::exportPropertyScopes($class->name) : '[]'; + if ( + $class?->hasMethod('__unserialize') + && !$class->getMethod('__unserialize')->getParameters()[0]->getType() + ) { + // fix contravariance type problem when $class declares a `__unserialize()` method without typehint. + $lazyProxyTraitStatement = <<__doUnserialize(\$data); + } + + EOPHP; + } else { + $lazyProxyTraitStatement = <<assertSame($expected, ProxyHelper::generateLazyProxy(null, [new \ReflectionClass(TestForProxyHelperInterface1::class), new \ReflectionClass(TestForProxyHelperInterface2::class)])); } + /** + * @dataProvider classWithUnserializeMagicMethodProvider + */ + public function testGenerateLazyProxyForClassWithUnserializeMagicMethod(object $obj, string $expected) + { + $this->assertStringContainsString($expected, ProxyHelper::generateLazyProxy(new \ReflectionClass($obj::class))); + } + + public static function classWithUnserializeMagicMethodProvider(): iterable + { + yield 'not type hinted __unserialize method' => [new class() { + public function __unserialize($array) + { + } + }, <<<'EOPHP' + implements \Symfony\Component\VarExporter\LazyObjectInterface + { + use \Symfony\Component\VarExporter\LazyProxyTrait { + __unserialize as private __doUnserialize; + } + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; + + public function __unserialize($data): void + { + $this->__doUnserialize($data); + } + } + EOPHP]; + + yield 'type hinted __unserialize method' => [new class() { + public function __unserialize(array $array) + { + } + }, <<<'EOPHP' + implements \Symfony\Component\VarExporter\LazyObjectInterface + { + use \Symfony\Component\VarExporter\LazyProxyTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; + } + EOPHP]; + } + public function testAttributes() { $expected = <<<'EOPHP' @@ -182,6 +226,7 @@ public function foo(#[\SensitiveParameter, AnotherAttribute] $a): int { } }); + $this->assertStringContainsString($expected, ProxyHelper::generateLazyProxy($class)); } 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