From 753ad20d6fa815b7a9754b1d9c26379cd4ca15ce Mon Sep 17 00:00:00 2001 From: Soha Jin Date: Tue, 5 Sep 2023 04:59:13 +0000 Subject: [PATCH 001/395] [DoctrineBridge] Add `message` to #[MapEntity] for NotFoundHttpException --- .../Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php | 2 +- src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php | 2 ++ .../Tests/ArgumentResolver/EntityValueResolverTest.php | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php index b531857c1422c..449da294dfb5e 100644 --- a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php +++ b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php @@ -73,7 +73,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array } if (null === $object && !$argument->isNullable()) { - throw new NotFoundHttpException(sprintf('"%s" object not found by "%s".', $options->class, self::class).$message); + throw new NotFoundHttpException($options->message ?? (sprintf('"%s" object not found by "%s".', $options->class, self::class).$message)); } return [$object]; diff --git a/src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php b/src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php index 529bf05dc7767..7f0c1fa335069 100644 --- a/src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php +++ b/src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php @@ -31,6 +31,7 @@ public function __construct( public ?bool $evictCache = null, bool $disabled = false, string $resolver = EntityValueResolver::class, + public ?string $message = null, ) { parent::__construct($resolver, $disabled); } @@ -46,6 +47,7 @@ public function withDefaults(self $defaults, ?string $class): static $clone->stripNull ??= $defaults->stripNull ?? false; $clone->id ??= $defaults->id; $clone->evictCache ??= $defaults->evictCache ?? false; + $clone->message ??= $defaults->message; return $clone; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php b/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php index 883af01280532..ef62a15f12c09 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php @@ -153,7 +153,7 @@ public function testResolveWithConversionFailedException() $request = new Request(); $request->attributes->set('id', 'test'); - $argument = $this->createArgument('stdClass', new MapEntity(id: 'id')); + $argument = $this->createArgument('stdClass', new MapEntity(id: 'id', message: 'Test')); $repository = $this->getMockBuilder(ObjectRepository::class)->getMock(); $repository->expects($this->once()) @@ -167,6 +167,7 @@ public function testResolveWithConversionFailedException() ->willReturn($repository); $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('Test'); $resolver->resolve($request, $argument); } From a48bac139df7bc1f8fc4fc2c3c719e6d22eddbd7 Mon Sep 17 00:00:00 2001 From: Rachid Hammaoui Date: Mon, 6 Nov 2023 22:58:29 +0100 Subject: [PATCH 002/395] [Messenger] Improve PHPDoc descriptions of attribute classes and parameters --- .../Messenger/Attribute/AsMessageHandler.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php b/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php index c0acf6f6c3d3b..e0d764e5c4cb2 100644 --- a/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php +++ b/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php @@ -20,10 +20,29 @@ class AsMessageHandler { public function __construct( + /** + * Name of the bus from which this handler can receive messages, by default all buses. + */ public ?string $bus = null, + + /** + * Name of the transport from which this handler can receive messages, by default all transports. + */ public ?string $fromTransport = null, + + /** + * Type of messages (FQCN) that can be processed by the handler, only needed if can't be guessed by type-hint. + */ public ?string $handles = null, + + /** + * Name of the method that will process the message, only if the target is a class. + */ public ?string $method = null, + + /** + * Priority of this handler when multiple handlers can process the same message. + */ public int $priority = 0, ) { } From 234c3c40ad5fd4f25e18814ce3c286ab76359f05 Mon Sep 17 00:00:00 2001 From: Julian Krzefski Date: Thu, 9 Nov 2023 13:15:54 +0100 Subject: [PATCH 003/395] Do not use hyphens in exception message --- .../Component/ErrorHandler/Resources/views/exception.html.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/ErrorHandler/Resources/views/exception.html.php b/src/Symfony/Component/ErrorHandler/Resources/views/exception.html.php index 31554a468d163..e5471dc6378f1 100644 --- a/src/Symfony/Component/ErrorHandler/Resources/views/exception.html.php +++ b/src/Symfony/Component/ErrorHandler/Resources/views/exception.html.php @@ -15,7 +15,7 @@
-

formatFileFromText(nl2br($exceptionMessage)); ?>

+

formatFileFromText(nl2br($exceptionMessage)); ?>

include('assets/images/symfony-ghost.svg.php'); ?> From c9e7809f045a1366afe2c2643bae15751ae7b500 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 15 Nov 2023 18:02:22 +0100 Subject: [PATCH 004/395] Bump version to 7.1 --- src/Symfony/Component/HttpKernel/Kernel.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index db2c1de9cc351..11950a5a522d6 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -76,15 +76,15 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.0.0-DEV'; - public const VERSION_ID = 70000; + public const VERSION = '7.1.0-DEV'; + public const VERSION_ID = 70100; public const MAJOR_VERSION = 7; - public const MINOR_VERSION = 0; + public const MINOR_VERSION = 1; public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; - public const END_OF_MAINTENANCE = '07/2024'; - public const END_OF_LIFE = '07/2024'; + public const END_OF_MAINTENANCE = '01/2025'; + public const END_OF_LIFE = '01/2025'; public function __construct(string $environment, bool $debug) { From 574b8a32379da0ae897e8b2d5aa782dbb08110c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 15 Nov 2023 23:01:24 +0100 Subject: [PATCH 005/395] Fix ProgressBar::iterate on empty iterator Co-authored-by: Florian Reimair --- .../Component/Console/Helper/ProgressBar.php | 59 ++++++++++++++----- .../Console/Tests/Helper/ProgressBarTest.php | 16 ++++- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 64389c4a2d285..a1f806d5f3113 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -195,7 +195,7 @@ public function getStartTime(): int public function getMaxSteps(): int { - return $this->max; + return $this->max ?? 0; } public function getProgress(): int @@ -215,7 +215,7 @@ public function getProgressPercent(): float public function getBarOffset(): float { - return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth); + return floor(null !== $this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth); } public function getEstimated(): float @@ -253,7 +253,7 @@ public function setBarCharacter(string $char): void public function getBarCharacter(): string { - return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar); + return $this->barChar ?? (null !== $this->max ? '=' : $this->emptyBarChar); } public function setEmptyBarCharacter(string $char): void @@ -315,7 +315,21 @@ public function maxSecondsBetweenRedraws(float $seconds): void */ public function iterate(iterable $iterable, int $max = null): iterable { - $this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0)); + if (0 === $max) { + $max = null; + } + + $max ??= is_countable($iterable) ? \count($iterable) : null; + + if (0 === $max) { + $this->max = 0; + $this->stepWidth = 2; + $this->finish(); + + return; + } + + $this->start($max); foreach ($iterable as $key => $value) { yield $key => $value; @@ -373,11 +387,15 @@ public function setProgress(int $step): void $step = 0; } - $redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10); - $prevPeriod = (int) ($this->step / $redrawFreq); - $currPeriod = (int) ($step / $redrawFreq); + $redrawFreq = $this->redrawFreq ?? (($this->max ?? 10) / 10); + $prevPeriod = $redrawFreq ? (int) ($this->step / $redrawFreq) : 0; + $currPeriod = $redrawFreq ? (int) ($step / $redrawFreq) : 0; $this->step = $step; - $this->percent = $this->max ? (float) $this->step / $this->max : 0; + $this->percent = match ($this->max) { + null => 0, + 0 => 1, + default => (float) $this->step / $this->max, + }; $timeInterval = microtime(true) - $this->lastWriteTime; // Draw regardless of other limits @@ -398,11 +416,20 @@ public function setProgress(int $step): void } } - public function setMaxSteps(int $max): void + public function setMaxSteps(?int $max): void { + if (0 === $max) { + $max = null; + } + $this->format = null; - $this->max = max(0, $max); - $this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4; + if (null === $max) { + $this->max = null; + $this->stepWidth = 4; + } else { + $this->max = max(0, $max); + $this->stepWidth = Helper::width((string) $this->max); + } } /** @@ -410,16 +437,16 @@ public function setMaxSteps(int $max): void */ public function finish(): void { - if (!$this->max) { + if (null === $this->max) { $this->max = $this->step; } - if ($this->step === $this->max && !$this->overwrite) { + if (($this->step === $this->max || null === $this->max) && !$this->overwrite) { // prevent double 100% output return; } - $this->setProgress($this->max); + $this->setProgress($this->max ?? $this->step); } /** @@ -542,14 +569,14 @@ private static function initPlaceholderFormatters(): array }, 'elapsed' => fn (self $bar) => Helper::formatTime(time() - $bar->getStartTime(), 2), 'remaining' => function (self $bar) { - if (!$bar->getMaxSteps()) { + if (null === $bar->getMaxSteps()) { throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); } return Helper::formatTime($bar->getRemaining(), 2); }, 'estimated' => function (self $bar) { - if (!$bar->getMaxSteps()) { + if (null === $bar->getMaxSteps()) { throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); } diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 4dff078ae72dd..cc7ed6c8823de 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -1092,6 +1092,20 @@ public function testIterateUncountable() ); } + public function testEmptyInputWithDebugFormat() + { + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->setFormat('%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%'); + + $this->assertEquals([], iterator_to_array($bar->iterate([]))); + + rewind($output->getStream()); + $this->assertEquals( + ' 0/0 [============================] 100% < 1 sec/< 1 sec', + stream_get_contents($output->getStream()) + ); + } + protected function getOutputStream($decorated = true, $verbosity = StreamOutput::VERBOSITY_NORMAL) { return new StreamOutput(fopen('php://memory', 'r+', false), $verbosity, $decorated); @@ -1263,7 +1277,7 @@ public function testMultiLineFormatIsFullyCorrectlyWithManuallyCleanup() 'Foo!'.\PHP_EOL. $this->generateOutput('[--->------------------------]'). "\nProcessing \"foobar\"...". - $this->generateOutput("[----->----------------------]\nProcessing \"foobar\"..."), + $this->generateOutput("[============================]\nProcessing \"foobar\"..."), stream_get_contents($output->getStream()) ); } From 137518de5d693c9d609944e4cf6a787546d79eab Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Fri, 17 Nov 2023 12:44:30 -0500 Subject: [PATCH 006/395] add argument to prepend extension config --- src/Symfony/Component/DependencyInjection/CHANGELOG.md | 5 +++++ .../Loader/Configurator/ContainerConfigurator.php | 8 +++++++- .../Tests/Extension/AbstractExtensionTest.php | 2 +- .../Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 8930b03ab6ff0..d3ae6c6c359c5 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add argument `$prepend` to `ContainerConfigurator::extension()` to prepend the configuration instead of appending it + 7.0 --- diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php index 883b5542ac51b..ad1110e17442a 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php @@ -48,8 +48,14 @@ public function __construct(ContainerBuilder $container, PhpFileLoader $loader, $this->env = $env; } - final public function extension(string $namespace, array $config): void + final public function extension(string $namespace, array $config, bool $prepend = false): void { + if ($prepend) { + $this->container->prependExtensionConfig($namespace, static::processValue($config)); + + return; + } + if (!$this->container->hasExtension($namespace)) { $extensions = array_filter(array_map(fn (ExtensionInterface $ext) => $ext->getAlias(), $this->container->getExtensions())); throw new InvalidArgumentException(sprintf('There is no extension able to load the configuration for "%s" (in "%s"). Looked for namespace "%s", found "%s".', $namespace, $this->file, $namespace, $extensions ? implode('", "', $extensions) : 'none')); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php b/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php index cb56983d365ce..9180ab8342da6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php @@ -63,7 +63,7 @@ public function prependExtension(ContainerConfigurator $container, ContainerBuil $container->extension('third', ['foo' => 'append']); // prepend config - $builder->prependExtensionConfig('third', ['foo' => 'prepend']); + $container->extension('third', ['foo' => 'prepend'], true); } }; diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php index 4fba6260f9337..959536ecbdc51 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php @@ -31,7 +31,7 @@ public function configure(DefinitionConfigurator $definition): void public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void { - $container->extension('loaded', ['bar' => 'baz']); + $container->extension('loaded', ['bar' => 'baz'], true); } public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void From 3286539bef534b7b375f79e7a9cc1b1dcc132f28 Mon Sep 17 00:00:00 2001 From: Javier Spagnoletti Date: Sun, 22 Oct 2023 00:30:44 -0300 Subject: [PATCH 007/395] [Yaml] Allow Yaml component to get all the enum cases --- src/Symfony/Component/Yaml/CHANGELOG.md | 5 ++++ src/Symfony/Component/Yaml/Inline.php | 27 ++++++++++++------- .../Component/Yaml/Tests/InlineTest.php | 18 ++++++++++--- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/Yaml/CHANGELOG.md b/src/Symfony/Component/Yaml/CHANGELOG.md index 4342bb3cd490c..74b0a71466611 100644 --- a/src/Symfony/Component/Yaml/CHANGELOG.md +++ b/src/Symfony/Component/Yaml/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add support for getting all the enum cases with `!php/enum Foo` + 7.0 --- diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index 382fa51c24a73..3c356efa6b708 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -643,24 +643,31 @@ private static function evaluateScalar(string $scalar, int $flags, array &$refer } $i = 0; - $enum = self::parseScalar(substr($scalar, 10), 0, null, $i, false); - if ($useValue = str_ends_with($enum, '->value')) { - $enum = substr($enum, 0, -7); - } - if (!\defined($enum)) { + $enumName = self::parseScalar(substr($scalar, 10), 0, null, $i, false); + $useName = str_contains($enumName, '::'); + $enum = $useName ? strstr($enumName, '::', true) : $enumName; + + if (!enum_exists($enum)) { throw new ParseException(sprintf('The enum "%s" is not defined.', $enum), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } + if (!$useName) { + return $enum::cases(); + } + if ($useValue = str_ends_with($enumName, '->value')) { + $enumName = substr($enumName, 0, -7); + } - $value = \constant($enum); - - if (!$value instanceof \UnitEnum) { - throw new ParseException(sprintf('The string "%s" is not the name of a valid enum.', $enum), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); + if (!\defined($enumName)) { + throw new ParseException(sprintf('The string "%s" is not the name of a valid enum.', $enumName), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } + + $value = \constant($enumName); + if (!$useValue) { return $value; } if (!$value instanceof \BackedEnum) { - throw new ParseException(sprintf('The enum "%s" defines no value next to its name.', $enum), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); + throw new ParseException(sprintf('The enum "%s" defines no value next to its name.', $enumName), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } return $value->value; diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index 62fbb6af41b34..34dce761ccefc 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -76,14 +76,21 @@ public function testParsePhpConstantThrowsExceptionWhenUndefined() public function testParsePhpEnumThrowsExceptionWhenUndefined() { $this->expectException(ParseException::class); - $this->expectExceptionMessage('The enum "SomeEnum::Foo" is not defined'); - Inline::parse('!php/enum SomeEnum::Foo', Yaml::PARSE_CONSTANT); + $this->expectExceptionMessage('The enum "SomeEnum" is not defined'); + Inline::parse('!php/enum SomeEnum', Yaml::PARSE_CONSTANT); + } + + public function testParsePhpEnumThrowsExceptionWhenNameUndefined() + { + $this->expectException(ParseException::class); + $this->expectExceptionMessage('The string "Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum::Foo" is not the name of a valid enum'); + Inline::parse('!php/enum Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum::Foo', Yaml::PARSE_CONSTANT); } public function testParsePhpEnumThrowsExceptionWhenNotAnEnum() { $this->expectException(ParseException::class); - $this->expectExceptionMessage('The string "PHP_INT_MAX" is not the name of a valid enum'); + $this->expectExceptionMessage('The enum "PHP_INT_MAX" is not defined'); Inline::parse('!php/enum PHP_INT_MAX', Yaml::PARSE_CONSTANT); } @@ -718,6 +725,11 @@ public function testDumpUnitEnum() $this->assertSame("!php/const Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum::BAR", Inline::dump(FooUnitEnum::BAR)); } + public function testParseUnitEnumCases() + { + $this->assertSame(FooUnitEnum::cases(), Inline::parse("!php/enum Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum", Yaml::PARSE_CONSTANT)); + } + public function testParseUnitEnum() { $this->assertSame(FooUnitEnum::BAR, Inline::parse("!php/enum Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum::BAR", Yaml::PARSE_CONSTANT)); From 3ce498d4f6bd9dde5afdfb2b38b55dd5e4afd044 Mon Sep 17 00:00:00 2001 From: MatTheCat Date: Sat, 18 Nov 2023 15:43:39 +0100 Subject: [PATCH 008/395] [Form] Deprecate not configuring the `default_protocol` option of the `UrlType` --- src/Symfony/Component/Form/CHANGELOG.md | 5 +++++ .../Component/Form/Extension/Core/Type/UrlType.php | 7 ++++++- .../Form/Tests/Extension/Core/Type/TextTypeTest.php | 8 ++++---- .../Form/Tests/Extension/Core/Type/UrlTypeTest.php | 13 +++++++++++++ .../Type/UrlTypeValidatorExtensionTest.php | 2 +- src/Symfony/Component/Form/composer.json | 1 + 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 6c712cc1413a9..1a10c50f93e0d 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + +* Deprecate not configuring the `default_protocol` option of the `UrlType`, it will default to `null` in 8.0 + 7.0 --- diff --git a/src/Symfony/Component/Form/Extension/Core/Type/UrlType.php b/src/Symfony/Component/Form/Extension/Core/Type/UrlType.php index d9cd3c6fb3c7e..fd6025729ae91 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/UrlType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/UrlType.php @@ -16,6 +16,7 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class UrlType extends AbstractType @@ -38,7 +39,11 @@ public function buildView(FormView $view, FormInterface $form, array $options): public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'default_protocol' => 'http', + 'default_protocol' => static function (Options $options) { + trigger_deprecation('symfony/form', '7.1', 'Not configuring the "default_protocol" option when using the UrlType is deprecated. It will default to "null" in 8.0.'); + + return 'http'; + }, 'invalid_message' => 'Please enter a valid URL.', ]); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TextTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TextTypeTest.php index e14a816362945..a28dfa9afa4b6 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TextTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TextTypeTest.php @@ -22,9 +22,9 @@ public function testSubmitNull($expected = null, $norm = null, $view = null) public function testSubmitNullReturnsNullWithEmptyDataAsString() { - $form = $this->factory->create(static::TESTED_TYPE, 'name', [ + $form = $this->factory->create(static::TESTED_TYPE, 'name', array_merge($this->getTestOptions(), [ 'empty_data' => '', - ]); + ])); $form->submit(null); $this->assertSame('', $form->getData()); @@ -48,9 +48,9 @@ public static function provideZeros(): array */ public function testSetDataThroughParamsWithZero($data, $dataAsString) { - $form = $this->factory->create(static::TESTED_TYPE, null, [ + $form = $this->factory->create(static::TESTED_TYPE, null, array_merge($this->getTestOptions(), [ 'data' => $data, - ]); + ])); $view = $form->createView(); $this->assertFalse($form->isEmpty()); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/UrlTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/UrlTypeTest.php index b9387d01a45e6..28e8b9ac746d6 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/UrlTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/UrlTypeTest.php @@ -11,14 +11,21 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class UrlTypeTest extends TextTypeTest { + use ExpectDeprecationTrait; + public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\UrlType'; + /** + * @group legacy + */ public function testSubmitAddsDefaultProtocolIfNoneIsIncluded() { + $this->expectDeprecation('Since symfony/form 7.1: Not configuring the "default_protocol" option when using the UrlType is deprecated. It will default to "null" in 8.0.'); $form = $this->factory->create(static::TESTED_TYPE, 'name'); $form->submit('www.domain.com'); @@ -86,6 +93,7 @@ public function testThrowExceptionIfDefaultProtocolIsInvalid() public function testSubmitNullUsesDefaultEmptyData($emptyData = 'empty', $expectedData = 'http://empty') { $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'default_protocol' => 'http', 'empty_data' => $emptyData, ]); $form->submit(null); @@ -95,4 +103,9 @@ public function testSubmitNullUsesDefaultEmptyData($emptyData = 'empty', $expect $this->assertSame($expectedData, $form->getNormData()); $this->assertSame($expectedData, $form->getData()); } + + protected function getTestOptions(): array + { + return ['default_protocol' => 'http']; + } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php index e6314a3c59a29..edb212cbd49eb 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php @@ -20,7 +20,7 @@ class UrlTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase protected function createForm(array $options = []) { - return $this->factory->create(UrlType::class, null, $options); + return $this->factory->create(UrlType::class, null, $options + ['default_protocol' => 'http']); } public function testInvalidMessage() diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 914ddc6c52b32..7ee167817f352 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher": "^6.4|^7.0", "symfony/options-resolver": "^6.4|^7.0", "symfony/polyfill-ctype": "~1.8", From 1439858fc8e266d1f9d149a2e554daa6e6475dd0 Mon Sep 17 00:00:00 2001 From: Marvin Petker Date: Mon, 30 Oct 2023 22:15:43 +0100 Subject: [PATCH 009/395] [DependencyInjection] Add `urlencode` function to `EnvVarProcessor` --- .../Component/DependencyInjection/CHANGELOG.md | 1 + .../Component/DependencyInjection/EnvVarProcessor.php | 5 +++++ .../Compiler/RegisterEnvVarProcessorsPassTest.php | 1 + .../DependencyInjection/Tests/EnvVarProcessorTest.php | 11 +++++++++++ 4 files changed, 18 insertions(+) diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 0f38ac86c63ae..c7e3e7b1f58e2 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Deprecate `ContainerAwareInterface` and `ContainerAwareTrait`, use dependency injection instead * Add `defined` env var processor that returns `true` for defined and neither null nor empty env vars * Add `#[AutowireLocator]` and `#[AutowireIterator]` attributes + * Add `urlencode` env var processor that url encodes a string value 6.3 --- diff --git a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php index bae5e289d7eca..7376d03595264 100644 --- a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php +++ b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php @@ -57,6 +57,7 @@ public static function getProvidedTypes(): array 'enum' => \BackedEnum::class, 'shuffle' => 'array', 'defined' => 'bool', + 'urlencode' => 'string', ]; } @@ -344,6 +345,10 @@ public function getEnv(string $prefix, string $name, \Closure $getEnv): mixed return trim($env); } + if ('urlencode' === $prefix) { + return rawurlencode($env); + } + throw new RuntimeException(sprintf('Unsupported env var prefix "%s" for env name "%s".', $prefix, $name)); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterEnvVarProcessorsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterEnvVarProcessorsPassTest.php index d23073c850bf8..f4a787b38b07f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterEnvVarProcessorsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterEnvVarProcessorsPassTest.php @@ -51,6 +51,7 @@ public function testSimpleProcessor() 'enum' => [\BackedEnum::class], 'shuffle' => ['array'], 'defined' => ['bool'], + 'urlencode' => ['string'], ]; $this->assertSame($expected, $container->getParameterBag()->getProvidedTypes()); diff --git a/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php b/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php index 8de0eaf8fc255..12a9d7606bb11 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php @@ -948,6 +948,17 @@ public function testGetEnvDefined(bool $expected, callable $callback) $this->assertSame($expected, (new EnvVarProcessor(new Container()))->getEnv('defined', 'NO_SOMETHING', $callback)); } + public function testGetEnvUrlencode() + { + $processor = new EnvVarProcessor(new Container()); + + $result = $processor->getEnv('urlencode', 'URLENCODETEST', function () { + return 'foo: Data123!@-_ + bar: Not the same content as Data123!@-_ +'; + }); + + $this->assertSame('foo%3A%20Data123%21%40-_%20%2B%20bar%3A%20Not%20the%20same%20content%20as%20Data123%21%40-_%20%2B', $result); + } + public static function provideGetEnvDefined(): iterable { yield 'Defined' => [true, fn () => 'foo']; From 7f4ed5c67200e539dfbbe24ab40deac8fe6e354a Mon Sep 17 00:00:00 2001 From: Bram Leeda Date: Fri, 20 Oct 2023 15:23:26 +0200 Subject: [PATCH 010/395] [String] New locale aware casing methods --- .../String/AbstractUnicodeString.php | 74 ++++++++++++ src/Symfony/Component/String/CHANGELOG.md | 5 + .../String/Tests/AbstractUnicodeTestCase.php | 114 ++++++++++++++++++ 3 files changed, 193 insertions(+) diff --git a/src/Symfony/Component/String/AbstractUnicodeString.php b/src/Symfony/Component/String/AbstractUnicodeString.php index df7265f3ebd15..af0532999018b 100644 --- a/src/Symfony/Component/String/AbstractUnicodeString.php +++ b/src/Symfony/Component/String/AbstractUnicodeString.php @@ -220,6 +220,21 @@ public function lower(): static return $str; } + /** + * @param string $locale In the format language_region (e.g. tr_TR) + */ + public function localeLower(string $locale): static + { + if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Lower')) { + $str = clone $this; + $str->string = $transliterator->transliterate($str->string); + + return $str; + } + + return $this->lower(); + } + public function match(string $regexp, int $flags = 0, int $offset = 0): array { $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; @@ -363,6 +378,21 @@ public function title(bool $allWords = false): static return $str; } + /** + * @param string $locale In the format language_region (e.g. tr_TR) + */ + public function localeTitle(string $locale): static + { + if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Title')) { + $str = clone $this; + $str->string = $transliterator->transliterate($str->string); + + return $str; + } + + return $this->title(); + } + public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { @@ -450,6 +480,21 @@ public function upper(): static return $str; } + /** + * @param string $locale In the format language_region (e.g. tr_TR) + */ + public function localeUpper(string $locale): static + { + if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Upper')) { + $str = clone $this; + $str->string = $transliterator->transliterate($str->string); + + return $str; + } + + return $this->upper(); + } + public function width(bool $ignoreAnsiDecoration = true): int { $width = 0; @@ -587,4 +632,33 @@ private function wcswidth(string $string): int return $width; } + + private function getLocaleTransliterator(string $locale, string $id): ?\Transliterator + { + $rule = $locale.'-'.$id; + if (\array_key_exists($rule, self::$transliterators)) { + return self::$transliterators[$rule]; + } + + if (null !== $transliterator = self::$transliterators[$rule] = \Transliterator::create($rule)) { + return $transliterator; + } + + // Try to find a parent locale (nl_BE -> nl) + if (false === $i = strpos($locale, '_')) { + return null; + } + + $parentRule = substr_replace($locale, '-'.$id, $i); + + // Parent locale was already cached, return and store as current locale + if (\array_key_exists($parentRule, self::$transliterators)) { + return self::$transliterators[$rule] = self::$transliterators[$parentRule]; + } + + // Create transliterator based on parent locale and cache the result on both initial and parent locale values + $transliterator = \Transliterator::create($parentRule); + + return self::$transliterators[$rule] = self::$transliterators[$parentRule] = $transliterator; + } } diff --git a/src/Symfony/Component/String/CHANGELOG.md b/src/Symfony/Component/String/CHANGELOG.md index 31a3b54dbf911..621cedfcddedf 100644 --- a/src/Symfony/Component/String/CHANGELOG.md +++ b/src/Symfony/Component/String/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `localeLower()`, `localeUpper()`, `localeTitle()` methods to `AbstractUnicodeString` + 6.2 --- diff --git a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php index 1ed16bca1cd6a..17461fc6388df 100644 --- a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php @@ -50,6 +50,48 @@ public function testAsciiClosureRule() $this->assertSame('Dieser Wert sollte grOEsser oder gleich', (string) $s->ascii([$rule])); } + /** + * @dataProvider provideLocaleLower + * + * @requires extension intl + */ + public function testLocaleLower(string $locale, string $expected, string $origin) + { + $instance = static::createFromString($origin)->localeLower($locale); + + $this->assertNotSame(static::createFromString($origin), $instance); + $this->assertEquals(static::createFromString($expected), $instance); + $this->assertSame($expected, (string) $instance); + } + + /** + * @dataProvider provideLocaleUpper + * + * @requires extension intl + */ + public function testLocaleUpper(string $locale, string $expected, string $origin) + { + $instance = static::createFromString($origin)->localeUpper($locale); + + $this->assertNotSame(static::createFromString($origin), $instance); + $this->assertEquals(static::createFromString($expected), $instance); + $this->assertSame($expected, (string) $instance); + } + + /** + * @dataProvider provideLocaleTitle + * + * @requires extension intl + */ + public function testLocaleTitle(string $locale, string $expected, string $origin) + { + $instance = static::createFromString($origin)->localeTitle($locale); + + $this->assertNotSame(static::createFromString($origin), $instance); + $this->assertEquals(static::createFromString($expected), $instance); + $this->assertSame($expected, (string) $instance); + } + public function provideCreateFromCodePoint(): array { return [ @@ -291,6 +333,78 @@ public static function provideLower(): array ); } + public static function provideLocaleLower(): array + { + return [ + // Lithuanian + // Introduce an explicit dot above when lowercasing capital I's and J's + // whenever there are more accents above. + // LATIN CAPITAL LETTER I WITH OGONEK -> LATIN SMALL LETTER I WITH OGONEK + ['lt', 'į', 'Į'], + // LATIN CAPITAL LETTER I WITH GRAVE -> LATIN SMALL LETTER I COMBINING DOT ABOVE + ['lt', 'i̇̀', 'Ì'], + // LATIN CAPITAL LETTER I WITH ACUTE -> LATIN SMALL LETTER I COMBINING DOT ABOVE COMBINING ACUTE ACCENT + ['lt', 'i̇́', 'Í'], + // LATIN CAPITAL LETTER I WITH TILDE -> LATIN SMALL LETTER I COMBINING DOT ABOVE COMBINING TILDE + ['lt', 'i̇̃', 'Ĩ'], + + // Turkish and Azeri + // When lowercasing, remove dot_above in the sequence I + dot_above, which will turn into 'i'. + // LATIN CAPITAL LETTER I WITH DOT ABOVE -> LATIN SMALL LETTER I + ['tr', 'i', 'İ'], + ['tr_TR', 'i', 'İ'], + ['az', 'i', 'İ'], + + // Default casing rules + // LATIN CAPITAL LETTER I WITH DOT ABOVE -> LATIN SMALL LETTER I COMBINING DOT ABOVE + ['en_US', 'i̇', 'İ'], + ['en', 'i̇', 'İ'], + ]; + } + + public static function provideLocaleUpper(): array + { + return [ + // Turkish and Azeri + // When uppercasing, i turns into a dotted capital I + // LATIN SMALL LETTER I -> LATIN CAPITAL LETTER I WITH DOT ABOVE + ['tr', 'İ', 'i'], + ['tr_TR', 'İ', 'i'], + ['az', 'İ', 'i'], + + // Greek + // Remove accents when uppercasing + // GREEK SMALL LETTER ALPHA WITH TONOS -> GREEK CAPITAL LETTER ALPHA + ['el', 'Α', 'ά'], + ['el_GR', 'Α', 'ά'], + + // Default casing rules + // GREEK SMALL LETTER ALPHA WITH TONOS -> GREEK CAPITAL LETTER ALPHA WITH TONOS + ['en_US', 'Ά', 'ά'], + ['en', 'Ά', 'ά'], + ]; + } + + public static function provideLocaleTitle(): array + { + return [ + // Greek + // Titlecasing words, should keep the accents on the first letter + ['el', 'Άδικος', 'άδικος'], + ['el_GR', 'Άδικος', 'άδικος'], + ['en', 'Άδικος', 'άδικος'], + + // Dutch + // Title casing should treat 'ij' as one character + ['nl_NL', 'IJssel', 'ijssel'], + ['nl_BE', 'IJssel', 'ijssel'], + ['nl', 'IJssel', 'ijssel'], + + // Default casing rules + ['en', 'Ijssel', 'ijssel'], + ]; + } + public static function provideUpper(): array { return array_merge( From dcca773f315c45c2496e4b12d31d67d7042137bf Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 20 Nov 2023 10:13:42 +0100 Subject: [PATCH 011/395] [Form] Add missing whitespace in Changelog --- src/Symfony/Component/Form/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 1a10c50f93e0d..273a71c0cde51 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 7.1 --- -* Deprecate not configuring the `default_protocol` option of the `UrlType`, it will default to `null` in 8.0 + * Deprecate not configuring the `default_protocol` option of the `UrlType`, it will default to `null` in 8.0 7.0 --- From 52b6416ff9539d9d11256de712abf1138ee40a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 17 Nov 2023 23:14:35 +0100 Subject: [PATCH 012/395] DotEnv debug command aware of custom dotenv_path Introduce SYMFONY_DOTENV_PATH set by DotEnv class and read by debug:dotenv command to contextualize the debug info with the file that was actually parsed. --- src/Symfony/Component/Dotenv/CHANGELOG.md | 5 ++ .../Component/Dotenv/Command/DebugCommand.php | 50 ++++++++++--------- .../Dotenv/Command/DotenvDumpCommand.php | 1 + src/Symfony/Component/Dotenv/Dotenv.php | 12 +++++ .../Dotenv/Tests/Command/DebugCommandTest.php | 42 +++++++++++++++- 5 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/Symfony/Component/Dotenv/CHANGELOG.md b/src/Symfony/Component/Dotenv/CHANGELOG.md index f3b7b7cf1e467..a587fbac59f2b 100644 --- a/src/Symfony/Component/Dotenv/CHANGELOG.md +++ b/src/Symfony/Component/Dotenv/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `SYMFONY_DOTENV_PATH` variable with the path to the `.env` file loaded by `Dotenv::loadEnv()` or `Dotenv::bootEnv()` + 6.2 --- diff --git a/src/Symfony/Component/Dotenv/Command/DebugCommand.php b/src/Symfony/Component/Dotenv/Command/DebugCommand.php index 1353969c38297..b544530486a46 100644 --- a/src/Symfony/Component/Dotenv/Command/DebugCommand.php +++ b/src/Symfony/Component/Dotenv/Command/DebugCommand.php @@ -70,21 +70,23 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $envFiles = $this->getEnvFiles(); - $availableFiles = array_filter($envFiles, fn (string $file) => is_file($this->getFilePath($file))); + $filePath = $_SERVER['SYMFONY_DOTENV_PATH'] ?? $this->projectDirectory.\DIRECTORY_SEPARATOR.'.env'; - if (\in_array('.env.local.php', $availableFiles, true)) { - $io->warning('Due to existing dump file (.env.local.php) all other dotenv files are skipped.'); + $envFiles = $this->getEnvFiles($filePath); + $availableFiles = array_filter($envFiles, fn (string $file) => is_file($file)); + + if (\in_array(sprintf('%s.local.php', $filePath), $availableFiles, true)) { + $io->warning(sprintf('Due to existing dump file (%s.local.php) all other dotenv files are skipped.', $this->getRelativeName($filePath))); } - if (is_file($this->getFilePath('.env')) && is_file($this->getFilePath('.env.dist'))) { - $io->warning('The file .env.dist gets skipped due to the existence of .env.'); + if (is_file($filePath) && is_file(sprintf('%s.dist', $filePath))) { + $io->warning(sprintf('The file %s.dist gets skipped due to the existence of %1$s.', $this->getRelativeName($filePath))); } $io->section('Scanned Files (in descending priority)'); - $io->listing(array_map(static fn (string $envFile) => \in_array($envFile, $availableFiles, true) - ? sprintf('✓ %s', $envFile) - : sprintf('⨯ %s', $envFile), $envFiles)); + $io->listing(array_map(fn (string $envFile) => \in_array($envFile, $availableFiles, true) + ? sprintf('✓ %s', $this->getRelativeName($envFile)) + : sprintf('⨯ %s', $this->getRelativeName($envFile)), $envFiles)); $nameFilter = $input->getArgument('filter'); $variables = $this->getVariables($availableFiles, $nameFilter); @@ -93,7 +95,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($variables || null === $nameFilter) { $io->table( - array_merge(['Variable', 'Value'], $availableFiles), + array_merge(['Variable', 'Value'], array_map($this->getRelativeName(...), $availableFiles)), $this->getVariables($availableFiles, $nameFilter) ); @@ -147,36 +149,38 @@ private function getAvailableVars(): array return $vars; } - private function getEnvFiles(): array + private function getEnvFiles(string $filePath): array { $files = [ - '.env.local.php', - sprintf('.env.%s.local', $this->kernelEnvironment), - sprintf('.env.%s', $this->kernelEnvironment), + sprintf('%s.local.php', $filePath), + sprintf('%s.%s.local', $filePath, $this->kernelEnvironment), + sprintf('%s.%s', $filePath, $this->kernelEnvironment), ]; if ('test' !== $this->kernelEnvironment) { - $files[] = '.env.local'; + $files[] = sprintf('%s.local', $filePath); } - if (!is_file($this->getFilePath('.env')) && is_file($this->getFilePath('.env.dist'))) { - $files[] = '.env.dist'; + if (!is_file($filePath) && is_file(sprintf('%s.dist', $filePath))) { + $files[] = sprintf('%s.dist', $filePath); } else { - $files[] = '.env'; + $files[] = $filePath; } return $files; } - private function getFilePath(string $file): string + private function getRelativeName(string $filePath): string { - return $this->projectDirectory.\DIRECTORY_SEPARATOR.$file; + if (str_starts_with($filePath, $this->projectDirectory)) { + return substr($filePath, \strlen($this->projectDirectory) + 1); + } + + return basename($filePath); } - private function loadValues(string $file): array + private function loadValues(string $filePath): array { - $filePath = $this->getFilePath($file); - if (str_ends_with($filePath, '.php')) { return include $filePath; } diff --git a/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php b/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php index 051f4b05a04b3..20f35c9b5b2f3 100644 --- a/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php +++ b/src/Symfony/Component/Dotenv/Command/DotenvDumpCommand.php @@ -107,6 +107,7 @@ private function loadEnv(string $dotenvPath, string $env, array $config): array try { $dotenv->loadEnv($dotenvPath, null, 'dev', $testEnvs); unset($_ENV['SYMFONY_DOTENV_VARS']); + unset($_ENV['SYMFONY_DOTENV_PATH']); return $_ENV; } finally { diff --git a/src/Symfony/Component/Dotenv/Dotenv.php b/src/Symfony/Component/Dotenv/Dotenv.php index 6e693ac28b329..3c27ec6e28410 100644 --- a/src/Symfony/Component/Dotenv/Dotenv.php +++ b/src/Symfony/Component/Dotenv/Dotenv.php @@ -100,6 +100,8 @@ public function load(string $path, string ...$extraPaths): void */ public function loadEnv(string $path, string $envKey = null, string $defaultEnv = 'dev', array $testEnvs = ['test'], bool $overrideExistingVars = false): void { + $this->populatePath($path); + $k = $envKey ?? $this->envKey; if (is_file($path) || !is_file($p = "$path.dist")) { @@ -144,6 +146,7 @@ public function bootEnv(string $path, string $defaultEnv = 'dev', array $testEnv $k = $this->envKey; if (\is_array($env) && ($overrideExistingVars || !isset($env[$k]) || ($_SERVER[$k] ?? $_ENV[$k] ?? $env[$k]) === $env[$k])) { + $this->populatePath($path); $this->populate($env, $overrideExistingVars); } else { $this->loadEnv($path, $k, $defaultEnv, $testEnvs, $overrideExistingVars); @@ -556,4 +559,13 @@ private function doLoad(bool $overrideExistingVars, array $paths): void $this->populate($this->parse(file_get_contents($path), $path), $overrideExistingVars); } } + + private function populatePath(string $path): void + { + $_ENV['SYMFONY_DOTENV_PATH'] = $_SERVER['SYMFONY_DOTENV_PATH'] = $path; + + if ($this->usePutenv) { + putenv('SYMFONY_DOTENV_PATH='.$path); + } + } } diff --git a/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php index 8bf787336574b..da5a5674034a8 100644 --- a/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php @@ -124,6 +124,28 @@ public function testScenario2InProdEnv() $this->assertStringContainsString('TEST 1234 1234 1234 0000', $output); } + public function testScenario2WithCustomPath() + { + $output = $this->executeCommand(__DIR__.'/Fixtures', 'prod', [], __DIR__.'/Fixtures/Scenario2/.env'); + + // Scanned Files + $this->assertStringContainsString('✓ Scenario2/.env.local.php', $output); + $this->assertStringContainsString('⨯ Scenario2/.env.prod.local', $output); + $this->assertStringContainsString('✓ Scenario2/.env.prod', $output); + $this->assertStringContainsString('⨯ Scenario2/.env.local', $output); + $this->assertStringContainsString('✓ Scenario2/.env.dist', $output); + + // Skipped Files + $this->assertStringNotContainsString('.env'.\PHP_EOL, $output); + $this->assertStringNotContainsString('.env.dev', $output); + $this->assertStringNotContainsString('.env.test', $output); + + // Variables + $this->assertStringContainsString('Variable Value Scenario2/.env.local.php Scenario2/.env.prod Scenario2/.env.dist', $output); + $this->assertStringContainsString('FOO BaR BaR BaR n/a', $output); + $this->assertStringContainsString('TEST 1234 1234 1234 0000', $output); + } + public function testWarningOnEnvAndEnvDistFile() { $output = $this->executeCommand(__DIR__.'/Fixtures/Scenario3', 'dev'); @@ -132,6 +154,14 @@ public function testWarningOnEnvAndEnvDistFile() $this->assertStringContainsString('[WARNING] The file .env.dist gets skipped', $output); } + public function testWarningOnEnvAndEnvDistFileWithCustomPath() + { + $output = $this->executeCommand(__DIR__.'/Fixtures', 'dev', dotenvPath: __DIR__.'/Fixtures/Scenario3/.env'); + + // Warning + $this->assertStringContainsString('[WARNING] The file Scenario3/.env.dist gets skipped', $output); + } + public function testWarningOnPhpEnvFile() { $output = $this->executeCommand(__DIR__.'/Fixtures/Scenario2', 'prod'); @@ -140,6 +170,14 @@ public function testWarningOnPhpEnvFile() $this->assertStringContainsString('[WARNING] Due to existing dump file (.env.local.php)', $output); } + public function testWarningOnPhpEnvFileWithCustomPath() + { + $output = $this->executeCommand(__DIR__.'/Fixtures', 'prod', dotenvPath: __DIR__.'/Fixtures/Scenario2/.env'); + + // Warning + $this->assertStringContainsString('[WARNING] Due to existing dump file (Scenario2/.env.local.php)', $output); + } + public function testScenario1InDevEnvWithNameFilter() { $output = $this->executeCommand(__DIR__.'/Fixtures/Scenario1', 'dev', ['filter' => 'FoO']); @@ -226,10 +264,10 @@ public function testCompletion() $this->assertSame(['FOO', 'TEST'], $tester->complete([''])); } - private function executeCommand(string $projectDirectory, string $env, array $input = []): string + private function executeCommand(string $projectDirectory, string $env, array $input = [], string $dotenvPath = null): string { $_SERVER['TEST_ENV_KEY'] = $env; - (new Dotenv('TEST_ENV_KEY'))->bootEnv($projectDirectory.'/.env'); + (new Dotenv('TEST_ENV_KEY'))->bootEnv($dotenvPath ?? $projectDirectory.'/.env'); $command = new DebugCommand($env, $projectDirectory); $command->setHelperSet(new HelperSet([new FormatterHelper()])); From b4fe683367b3ccebff6cf67e5442d003a0b814d2 Mon Sep 17 00:00:00 2001 From: Herberto Graca Date: Sun, 9 Jul 2023 22:25:32 +0200 Subject: [PATCH 013/395] [Messenger] Add `SKIP LOCKED` to the query that retrieves messages --- .../Messenger/Bridge/Doctrine/CHANGELOG.md | 5 + .../Tests/Transport/ConnectionTest.php | 93 ++++++++++++++++--- .../Bridge/Doctrine/Transport/Connection.php | 55 +++++++---- 3 files changed, 123 insertions(+), 30 deletions(-) diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Component/Messenger/Bridge/Doctrine/CHANGELOG.md index aaed24815e830..8a9716712c156 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Use `SKIP LOCKED` in the doctrine transport for MySQL, PostgreSQL and MSSQL + 5.1.0 ----- diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php index 0846cbb173b98..e69842f2bc3cb 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php @@ -14,10 +14,15 @@ use Doctrine\DBAL\Connection as DBALConnection; use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\MariaDb1060Platform; use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\MySQL57Platform; +use Doctrine\DBAL\Platforms\MySQL80Platform; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; +use Doctrine\DBAL\Platforms\PostgreSQL100Platform; +use Doctrine\DBAL\Platforms\PostgreSQL94Platform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Platforms\SQLServer2012Platform; use Doctrine\DBAL\Platforms\SQLServerPlatform; use Doctrine\DBAL\Query\QueryBuilder; @@ -391,28 +396,94 @@ class_exists(MySQLPlatform::class) ? new MySQLPlatform() : new MySQL57Platform() 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE', ]; + if (class_exists(MySQL80Platform::class) && !method_exists(QueryBuilder::class, 'forUpdate')) { + yield 'MySQL8 & DBAL<3.8' => [ + new MySQL80Platform(), + 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE', + ]; + } + + if (class_exists(MySQL80Platform::class) && method_exists(QueryBuilder::class, 'forUpdate')) { + yield 'MySQL8 & DBAL>=3.8' => [ + new MySQL80Platform(), + 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED', + ]; + } + yield 'MariaDB' => [ new MariaDBPlatform(), 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE', ]; - yield 'SQL Server' => [ - class_exists(SQLServerPlatform::class) && !class_exists(SQLServer2012Platform::class) ? new SQLServerPlatform() : new SQLServer2012Platform(), - 'SELECT m.* FROM messenger_messages m WITH (UPDLOCK, ROWLOCK) WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ', - ]; + if (class_exists(MariaDb1060Platform::class)) { + yield 'MariaDB106' => [ + new MariaDb1060Platform(), + 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED', + ]; + } - if (!class_exists(MySQL57Platform::class)) { - // DBAL >= 4 - yield 'Oracle' => [ - new OraclePlatform(), - 'SELECT w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", w.created_at AS "created_at", w.available_at AS "available_at", w.delivered_at AS "delivered_at" FROM messenger_messages w WHERE w.id IN (SELECT m.id FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC FETCH NEXT 1 ROWS ONLY) FOR UPDATE', + if (class_exists(MySQL57Platform::class)) { + yield 'Postgres & DBAL<4' => [ + new PostgreSQLPlatform(), + 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE', ]; } else { - // DBAL < 4 - yield 'Oracle' => [ + yield 'Postgres & DBAL>=4' => [ + new PostgreSQLPlatform(), + 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED', + ]; + } + + if (class_exists(PostgreSQL94Platform::class)) { + yield 'Postgres94' => [ + new PostgreSQL94Platform(), + 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE', + ]; + } + + if (class_exists(PostgreSQL100Platform::class) && !method_exists(QueryBuilder::class, 'forUpdate')) { + yield 'Postgres10 & DBAL<3.8' => [ + new PostgreSQL100Platform(), + 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE', + ]; + } + + if (class_exists(PostgreSQL100Platform::class) && method_exists(QueryBuilder::class, 'forUpdate')) { + yield 'Postgres10 & DBAL>=3.8' => [ + new PostgreSQL100Platform(), + 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED', + ]; + } + + if (!method_exists(QueryBuilder::class, 'forUpdate')) { + yield 'SQL Server & DBAL<3.8' => [ + class_exists(SQLServerPlatform::class) && !class_exists(SQLServer2012Platform::class) ? new SQLServerPlatform() : new SQLServer2012Platform(), + 'SELECT m.* FROM messenger_messages m WITH (UPDLOCK, ROWLOCK) WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ', + ]; + } + + if (method_exists(QueryBuilder::class, 'forUpdate')) { + yield 'SQL Server & DBAL>=3.8' => [ + class_exists(SQLServerPlatform::class) && !class_exists(SQLServer2012Platform::class) ? new SQLServerPlatform() : new SQLServer2012Platform(), + 'SELECT m.* FROM messenger_messages m WITH (UPDLOCK, ROWLOCK, READPAST) WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ', + ]; + } + + if (!method_exists(QueryBuilder::class, 'forUpdate')) { + yield 'Oracle & DBAL<3.8' => [ new OraclePlatform(), 'SELECT w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", w.created_at AS "created_at", w.available_at AS "available_at", w.delivered_at AS "delivered_at" FROM messenger_messages w WHERE w.id IN (SELECT a.id FROM (SELECT m.id FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC) a WHERE ROWNUM <= 1) FOR UPDATE', ]; + } elseif (class_exists(MySQL57Platform::class)) { + yield 'Oracle & 3.8<=DBAL<4' => [ + new OraclePlatform(), + 'SELECT w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", w.created_at AS "created_at", w.available_at AS "available_at", w.delivered_at AS "delivered_at" FROM messenger_messages w WHERE w.id IN (SELECT a.id FROM (SELECT m.id FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC) a WHERE ROWNUM <= 1) FOR UPDATE SKIP LOCKED', + ]; + } else { + yield 'Oracle & DBAL>=4' => [ + new OraclePlatform(), + 'SELECT w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", w.created_at AS "created_at", w.available_at AS "available_at", w.delivered_at AS "delivered_at" FROM messenger_messages w WHERE w.id IN (SELECT m.id FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC FETCH NEXT 1 ROWS ONLY) FOR UPDATE SKIP LOCKED', + ]; } } diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php index 59de234de4586..ec5af418f6cc1 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php @@ -18,6 +18,7 @@ use Doctrine\DBAL\LockMode; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; +use Doctrine\DBAL\Query\ForUpdate\ConflictResolutionMode; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\Schema; @@ -32,6 +33,8 @@ * * @author Vincent Touzet * @author Kévin Dunglas + * @author Herberto Graca + * @author Alexander Malyk */ class Connection implements ResetInterface { @@ -178,28 +181,22 @@ public function get(): ?array ->where('w.id IN ('.str_replace('SELECT a.* FROM', 'SELECT a.id FROM', $sql).')') ->setParameters($query->getParameters()); - if (method_exists(QueryBuilder::class, 'forUpdate')) { - $query->forUpdate(); - } - $sql = $query->getSQL(); - } elseif (method_exists(QueryBuilder::class, 'forUpdate')) { - $query->forUpdate(); - try { - $sql = $query->getSQL(); - } catch (DBALException $e) { - } - } elseif (preg_match('/FROM (.+) WHERE/', (string) $sql, $matches)) { - $fromClause = $matches[1]; - $sql = str_replace( - sprintf('FROM %s WHERE', $fromClause), - sprintf('FROM %s WHERE', $this->driverConnection->getDatabasePlatform()->appendLockHint($fromClause, LockMode::PESSIMISTIC_WRITE)), - $sql - ); } - // use SELECT ... FOR UPDATE to lock table - if (!method_exists(QueryBuilder::class, 'forUpdate')) { + if (method_exists(QueryBuilder::class, 'forUpdate')) { + $sql = $this->addLockMode($query, $sql); + } else { + if (preg_match('/FROM (.+) WHERE/', (string) $sql, $matches)) { + $fromClause = $matches[1]; + $sql = str_replace( + sprintf('FROM %s WHERE', $fromClause), + sprintf('FROM %s WHERE', $this->driverConnection->getDatabasePlatform()->appendLockHint($fromClause, LockMode::PESSIMISTIC_WRITE)), + $sql + ); + } + + // use SELECT ... FOR UPDATE to lock table $sql .= ' '.$this->driverConnection->getDatabasePlatform()->getWriteLockSQL(); } @@ -493,4 +490,24 @@ private function updateSchema(): void } } } + + private function addLockMode(QueryBuilder $query, string $sql): string + { + $query->forUpdate(ConflictResolutionMode::SKIP_LOCKED); + try { + return $query->getSQL(); + } catch (DBALException) { + return $this->fallBackToForUpdate($query, $sql); + } + } + + private function fallBackToForUpdate(QueryBuilder $query, string $sql): string + { + $query->forUpdate(); + try { + return $query->getSQL(); + } catch (DBALException) { + return $sql; + } + } } From a300de8615e58e3620d2b28e5007b92105848a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Auswo=CC=88ger?= Date: Wed, 1 Nov 2023 17:49:58 +0100 Subject: [PATCH 014/395] [Process] Pass the commandline as array to `proc_open()` --- .../HttpClient/Tests/HttpClientTestCase.php | 7 ++- .../Exception/ProcessStartFailedException.php | 45 ++++++++++++++ .../Process/Messenger/RunProcessContext.php | 4 +- src/Symfony/Component/Process/Process.php | 61 +++++++++++++------ .../RunProcessMessageHandlerTest.php | 6 +- .../Component/Process/Tests/ProcessTest.php | 23 +++++++ 6 files changed, 122 insertions(+), 24 deletions(-) create mode 100644 src/Symfony/Component/Process/Exception/ProcessStartFailedException.php diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index b7eac0f82a2aa..48fa25eb27d68 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -327,7 +327,12 @@ private static function startVulcain(HttpClientInterface $client) 'KEY_FILE' => __DIR__.'/Fixtures/tls/server.key', 'CERT_FILE' => __DIR__.'/Fixtures/tls/server.crt', ]); - $process->start(); + + try { + $process->start(); + } catch (ProcessFailedException $e) { + self::markTestSkipped('vulcain failed: '.$e->getMessage()); + } register_shutdown_function($process->stop(...)); sleep('\\' === \DIRECTORY_SEPARATOR ? 10 : 1); diff --git a/src/Symfony/Component/Process/Exception/ProcessStartFailedException.php b/src/Symfony/Component/Process/Exception/ProcessStartFailedException.php new file mode 100644 index 0000000000000..9bd5a036edcdc --- /dev/null +++ b/src/Symfony/Component/Process/Exception/ProcessStartFailedException.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception for processes failed during startup. + */ +class ProcessStartFailedException extends ProcessFailedException +{ + private Process $process; + + public function __construct(Process $process, ?string $message) + { + if ($process->isStarted()) { + throw new InvalidArgumentException('Expected a process that failed during startup, but the given process was started successfully.'); + } + + $error = sprintf('The command "%s" failed.'."\n\nWorking directory: %s\n\nError: %s", + $process->getCommandLine(), + $process->getWorkingDirectory(), + $message ?? 'unknown' + ); + + // Skip parent constructor + RuntimeException::__construct($error); + + $this->process = $process; + } + + public function getProcess(): Process + { + return $this->process; + } +} diff --git a/src/Symfony/Component/Process/Messenger/RunProcessContext.php b/src/Symfony/Component/Process/Messenger/RunProcessContext.php index b5ade07223007..5e223040419d1 100644 --- a/src/Symfony/Component/Process/Messenger/RunProcessContext.php +++ b/src/Symfony/Component/Process/Messenger/RunProcessContext.php @@ -27,7 +27,7 @@ public function __construct( Process $process, ) { $this->exitCode = $process->getExitCode(); - $this->output = $process->isOutputDisabled() ? null : $process->getOutput(); - $this->errorOutput = $process->isOutputDisabled() ? null : $process->getErrorOutput(); + $this->output = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getOutput(); + $this->errorOutput = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getErrorOutput(); } } diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 6b73c31d42a05..0b29a646c65dc 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -15,6 +15,7 @@ use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessSignaledException; +use Symfony\Component\Process\Exception\ProcessStartFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\Pipes\UnixPipes; @@ -233,11 +234,11 @@ public function __clone() * * @return int The exit status code * - * @throws RuntimeException When process can't be launched - * @throws RuntimeException When process is already running - * @throws ProcessTimedOutException When process timed out - * @throws ProcessSignaledException When process stopped after receiving signal - * @throws LogicException In case a callback is provided and output has been disabled + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running + * @throws ProcessTimedOutException When process timed out + * @throws ProcessSignaledException When process stopped after receiving signal + * @throws LogicException In case a callback is provided and output has been disabled * * @final */ @@ -284,9 +285,9 @@ public function mustRun(callable $callback = null, array $env = []): static * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * - * @throws RuntimeException When process can't be launched - * @throws RuntimeException When process is already running - * @throws LogicException In case a callback is provided and output has been disabled + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running + * @throws LogicException In case a callback is provided and output has been disabled */ public function start(callable $callback = null, array $env = []): void { @@ -306,12 +307,7 @@ public function start(callable $callback = null, array $env = []): void $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv(); if (\is_array($commandline = $this->commandline)) { - $commandline = implode(' ', array_map($this->escapeArgument(...), $commandline)); - - if ('\\' !== \DIRECTORY_SEPARATOR) { - // exec is mandatory to deal with sending a signal to the process - $commandline = 'exec '.$commandline; - } + $commandline = array_values(array_map(strval(...), $commandline)); } else { $commandline = $this->replacePlaceholders($commandline, $env); } @@ -322,6 +318,11 @@ public function start(callable $callback = null, array $env = []): void // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors[3] = ['pipe', 'w']; + if (\is_array($commandline)) { + // exec is mandatory to deal with sending a signal to the process + $commandline = 'exec '.$this->buildShellCommandline($commandline); + } + // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code'; @@ -338,10 +339,20 @@ public function start(callable $callback = null, array $env = []): void throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd)); } - $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + $lastError = null; + set_error_handler(function ($type, $msg) use (&$lastError) { + $lastError = $msg; + + return true; + }); + try { + $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + } finally { + restore_error_handler(); + } if (!\is_resource($process)) { - throw new RuntimeException('Unable to launch a new process.'); + throw new ProcessStartFailedException($this, $lastError); } $this->process = $process; $this->status = self::STATUS_STARTED; @@ -366,8 +377,8 @@ public function start(callable $callback = null, array $env = []): void * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * - * @throws RuntimeException When process can't be launched - * @throws RuntimeException When process is already running + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running * * @see start() * @@ -943,7 +954,7 @@ public function getLastOutputTime(): ?float */ public function getCommandLine(): string { - return \is_array($this->commandline) ? implode(' ', array_map($this->escapeArgument(...), $this->commandline)) : $this->commandline; + return $this->buildShellCommandline($this->commandline); } /** @@ -1472,8 +1483,18 @@ private function doSignal(int $signal, bool $throwException): bool return true; } - private function prepareWindowsCommandLine(string $cmd, array &$env): string + private function buildShellCommandline(string|array $commandline): string + { + if (\is_string($commandline)) { + return $commandline; + } + + return implode(' ', array_map($this->escapeArgument(...), $commandline)); + } + + private function prepareWindowsCommandLine(string|array $cmd, array &$env): string { + $cmd = $this->buildShellCommandline($cmd); $uid = uniqid('', true); $cmd = preg_replace_callback( '/"(?:( diff --git a/src/Symfony/Component/Process/Tests/Messenger/RunProcessMessageHandlerTest.php b/src/Symfony/Component/Process/Tests/Messenger/RunProcessMessageHandlerTest.php index 049da77a6ed0c..e095fa09d9bd4 100644 --- a/src/Symfony/Component/Process/Tests/Messenger/RunProcessMessageHandlerTest.php +++ b/src/Symfony/Component/Process/Tests/Messenger/RunProcessMessageHandlerTest.php @@ -33,7 +33,11 @@ public function testRunFailedProcess() (new RunProcessMessageHandler())(new RunProcessMessage(['invalid'])); } catch (RunProcessFailedException $e) { $this->assertSame(['invalid'], $e->context->message->command); - $this->assertSame('\\' === \DIRECTORY_SEPARATOR ? 1 : 127, $e->context->exitCode); + $this->assertContains( + $e->context->exitCode, + [null, '\\' === \DIRECTORY_SEPARATOR ? 1 : 127], + 'Exit code should be 1 on Windows, 127 on other systems, or null', + ); return; } diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index 44fb54ee7f907..dfb4fd2936959 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessSignaledException; +use Symfony\Component\Process\Exception\ProcessStartFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\InputStream; @@ -66,6 +67,28 @@ public function testInvalidCwd() $cmd->run(); } + /** + * @dataProvider invalidProcessProvider + */ + public function testInvalidCommand(Process $process) + { + try { + $this->assertSame('\\' === \DIRECTORY_SEPARATOR ? 1 : 127, $process->run()); + } catch (ProcessStartFailedException $e) { + // An invalid command might already fail during start since PHP 8.3 for platforms + // supporting posix_spawn(), see https://github.com/php/php-src/issues/12589 + $this->assertStringContainsString('No such file or directory', $e->getMessage()); + } + } + + public function invalidProcessProvider() + { + return [ + [new Process(['invalid'])], + [Process::fromShellCommandline('invalid')], + ]; + } + /** * @group transient-on-windows */ From dac592b513778b2e7de3450c5ce6ef1d4a2af4bd Mon Sep 17 00:00:00 2001 From: Daniel Burger <48986191+danielburger1337@users.noreply.github.com> Date: Wed, 8 Nov 2023 05:54:37 +0100 Subject: [PATCH 015/395] [HttpFoundation] Add `UploadedFile::getClientOriginalPath()` to support directory uploads --- .../Component/Form/NativeRequestHandler.php | 11 +-- .../Form/Tests/NativeRequestHandlerTest.php | 4 + .../Component/HttpFoundation/CHANGELOG.md | 5 ++ .../HttpFoundation/File/UploadedFile.php | 17 +++++ .../Component/HttpFoundation/FileBag.php | 22 +++--- .../Fixtures/webkitdirectory/nested/test.txt | 1 + .../File/Fixtures/webkitdirectory/test.txt | 1 + .../Tests/File/UploadedFileTest.php | 22 ++++++ .../HttpFoundation/Tests/FileBagTest.php | 76 +++++++++++++------ 9 files changed, 120 insertions(+), 39 deletions(-) create mode 100644 src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/nested/test.txt create mode 100644 src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/test.txt diff --git a/src/Symfony/Component/Form/NativeRequestHandler.php b/src/Symfony/Component/Form/NativeRequestHandler.php index 9ac1f2ea9ae27..8c74bd1ded8ae 100644 --- a/src/Symfony/Component/Form/NativeRequestHandler.php +++ b/src/Symfony/Component/Form/NativeRequestHandler.php @@ -29,6 +29,7 @@ class NativeRequestHandler implements RequestHandlerInterface */ private const FILE_KEYS = [ 'error', + 'full_path', 'name', 'size', 'tmp_name', @@ -186,9 +187,7 @@ private static function fixPhpFilesArray(mixed $data): mixed return $data; } - // Remove extra key added by PHP 8.1. - unset($data['full_path']); - $keys = array_keys($data); + $keys = array_keys($data + ['full_path' => null]); sort($keys); if (self::FILE_KEYS !== $keys || !isset($data['name']) || !\is_array($data['name'])) { @@ -207,7 +206,9 @@ private static function fixPhpFilesArray(mixed $data): mixed 'type' => $data['type'][$key], 'tmp_name' => $data['tmp_name'][$key], 'size' => $data['size'][$key], - ]); + ] + (isset($data['full_path'][$key]) ? [ + 'full_path' => $data['full_path'][$key], + ] : [])); } return $files; @@ -219,7 +220,7 @@ private static function stripEmptyFiles(mixed $data): mixed return $data; } - $keys = array_keys($data); + $keys = array_keys($data + ['full_path' => null]); sort($keys); if (self::FILE_KEYS === $keys) { diff --git a/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php b/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php index bdb0763f9d50f..679c3366d8256 100644 --- a/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php +++ b/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php @@ -99,6 +99,9 @@ public function testFixBuggyFilesArray() 'name' => [ 'field' => 'upload.txt', ], + 'full_path' => [ + 'field' => 'path/to/file/upload.txt', + ], 'type' => [ 'field' => 'text/plain', ], @@ -118,6 +121,7 @@ public function testFixBuggyFilesArray() $this->assertTrue($form->isSubmitted()); $this->assertEquals([ 'name' => 'upload.txt', + 'full_path' => 'path/to/file/upload.txt', 'type' => 'text/plain', 'tmp_name' => 'owfdskjasdfsa', 'error' => \UPLOAD_ERR_OK, diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 1a3ef0e411ea1..d4d07411f70e7 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `UploadedFile::getClientOriginalPath()` + 7.0 --- diff --git a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php index e27cf3812d3c5..b0a01f30f68b6 100644 --- a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php +++ b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php @@ -35,6 +35,7 @@ class UploadedFile extends File private string $originalName; private string $mimeType; private int $error; + private string $originalPath; /** * Accepts the information of the uploaded file as provided by the PHP global $_FILES. @@ -63,6 +64,7 @@ class UploadedFile extends File public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false) { $this->originalName = $this->getName($originalName); + $this->originalPath = strtr($originalName, '\\', '/'); $this->mimeType = $mimeType ?: 'application/octet-stream'; $this->error = $error ?: \UPLOAD_ERR_OK; $this->test = $test; @@ -92,6 +94,21 @@ public function getClientOriginalExtension(): string return pathinfo($this->originalName, \PATHINFO_EXTENSION); } + /** + * Returns the original file full path. + * + * It is extracted from the request from which the file has been uploaded. + * This should not be considered as a safe value to use for a file name/path on your servers. + * + * If this file was uploaded with the "webkitdirectory" upload directive, this will contain + * the path of the file relative to the uploaded root directory. Otherwise this will be identical + * to getClientOriginalName(). + */ + public function getClientOriginalPath(): string + { + return $this->originalPath; + } + /** * Returns the file mime type. * diff --git a/src/Symfony/Component/HttpFoundation/FileBag.php b/src/Symfony/Component/HttpFoundation/FileBag.php index 0541750bb2304..561e7cdea7912 100644 --- a/src/Symfony/Component/HttpFoundation/FileBag.php +++ b/src/Symfony/Component/HttpFoundation/FileBag.php @@ -21,7 +21,7 @@ */ class FileBag extends ParameterBag { - private const FILE_KEYS = ['error', 'name', 'size', 'tmp_name', 'type']; + private const FILE_KEYS = ['error', 'full_path', 'name', 'size', 'tmp_name', 'type']; /** * @param array|UploadedFile[] $parameters An array of HTTP files @@ -65,18 +65,18 @@ protected function convertFileInformation(array|UploadedFile $file): array|Uploa } $file = $this->fixPhpFilesArray($file); - $keys = array_keys($file); + $keys = array_keys($file + ['full_path' => null]); sort($keys); - if (self::FILE_KEYS == $keys) { - if (\UPLOAD_ERR_NO_FILE == $file['error']) { + if (self::FILE_KEYS === $keys) { + if (\UPLOAD_ERR_NO_FILE === $file['error']) { $file = null; } else { - $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error'], false); + $file = new UploadedFile($file['tmp_name'], $file['full_path'] ?? $file['name'], $file['type'], $file['error'], false); } } else { $file = array_map(fn ($v) => $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v, $file); - if (array_keys($keys) === $keys) { + if (array_is_list($file)) { $file = array_filter($file); } } @@ -98,12 +98,10 @@ protected function convertFileInformation(array|UploadedFile $file): array|Uploa */ protected function fixPhpFilesArray(array $data): array { - // Remove extra key added by PHP 8.1. - unset($data['full_path']); - $keys = array_keys($data); + $keys = array_keys($data + ['full_path' => null]); sort($keys); - if (self::FILE_KEYS != $keys || !isset($data['name']) || !\is_array($data['name'])) { + if (self::FILE_KEYS !== $keys || !isset($data['name']) || !\is_array($data['name'])) { return $data; } @@ -119,7 +117,9 @@ protected function fixPhpFilesArray(array $data): array 'type' => $data['type'][$key], 'tmp_name' => $data['tmp_name'][$key], 'size' => $data['size'][$key], - ]); + ] + (isset($data['full_path'][$key]) ? [ + 'full_path' => $data['full_path'][$key], + ] : [])); } return $files; diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/nested/test.txt b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/nested/test.txt new file mode 100644 index 0000000000000..83e5e03f72d03 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/nested/test.txt @@ -0,0 +1 @@ +nested webkitdirectory text diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/test.txt b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/test.txt new file mode 100644 index 0000000000000..0d872b4804a73 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/test.txt @@ -0,0 +1 @@ +webkitdirectory text diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php b/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php index 69179fc37ef74..9c18ad1839420 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php @@ -322,4 +322,26 @@ public function testGetMaxFilesize() $this->assertSame(\PHP_INT_MAX, $size); } } + + public function testgetClientOriginalPath() + { + $file = new UploadedFile( + __DIR__.'/Fixtures/test.gif', + 'test.gif', + 'image/gif' + ); + + $this->assertEquals('test.gif', $file->getClientOriginalPath()); + } + + public function testgetClientOriginalPathWebkitDirectory() + { + $file = new UploadedFile( + __DIR__.'/Fixtures/webkitdirectory/test.txt', + 'webkitdirectory/test.txt', + 'text/plain', + ); + + $this->assertEquals('webkitdirectory/test.txt', $file->getClientOriginalPath()); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/FileBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/FileBagTest.php index b12621e7dd464..1afc61d2ad64e 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/FileBagTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/FileBagTest.php @@ -32,27 +32,12 @@ public function testFileMustBeAnArrayOrUploadedFile() public function testShouldConvertsUploadedFiles() { $tmpFile = $this->createTempFile(); - $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain'); + $name = basename($tmpFile); - $bag = new FileBag(['file' => [ - 'name' => basename($tmpFile), - 'type' => 'text/plain', - 'tmp_name' => $tmpFile, - 'error' => 0, - 'size' => null, - ]]); - - $this->assertEquals($file, $bag->get('file')); - } - - public function testShouldConvertsUploadedFilesPhp81() - { - $tmpFile = $this->createTempFile(); - $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain'); + $file = new UploadedFile($tmpFile, $name, 'text/plain'); $bag = new FileBag(['file' => [ - 'name' => basename($tmpFile), - 'full_path' => basename($tmpFile), + 'name' => $name, 'type' => 'text/plain', 'tmp_name' => $tmpFile, 'error' => 0, @@ -104,12 +89,13 @@ public function testShouldNotRemoveEmptyUploadedFilesForAssociativeArray() public function testShouldConvertUploadedFilesWithPhpBug() { $tmpFile = $this->createTempFile(); - $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain'); + $name = basename($tmpFile); + $file = new UploadedFile($tmpFile, $name, 'text/plain'); $bag = new FileBag([ 'child' => [ 'name' => [ - 'file' => basename($tmpFile), + 'file' => $name, ], 'type' => [ 'file' => 'text/plain', @@ -133,12 +119,13 @@ public function testShouldConvertUploadedFilesWithPhpBug() public function testShouldConvertNestedUploadedFilesWithPhpBug() { $tmpFile = $this->createTempFile(); - $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain'); + $name = basename($tmpFile); + $file = new UploadedFile($tmpFile, $name, 'text/plain'); $bag = new FileBag([ 'child' => [ 'name' => [ - 'sub' => ['file' => basename($tmpFile)], + 'sub' => ['file' => $name], ], 'type' => [ 'sub' => ['file' => 'text/plain'], @@ -162,13 +149,56 @@ public function testShouldConvertNestedUploadedFilesWithPhpBug() public function testShouldNotConvertNestedUploadedFiles() { $tmpFile = $this->createTempFile(); - $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain'); + $name = basename($tmpFile); + $file = new UploadedFile($tmpFile, $name, 'text/plain'); $bag = new FileBag(['image' => ['file' => $file]]); $files = $bag->all(); $this->assertEquals($file, $files['image']['file']); } + public function testWebkitDirectoryUpload() + { + $file1 = __DIR__.'/File/Fixtures/webkitdirectory/test.txt'; + $file2 = __DIR__.'/File/Fixtures/webkitdirectory/nested/test.txt'; + + $bag = new FileBag([ + 'child' => [ + 'name' => [ + 'test.txt', + 'test.txt', + ], + 'full_path' => [ + 'webkitdirectory/test.txt', + 'webkitdirectory/nested/test.txt', + ], + 'type' => [ + 'text/plain', + 'text/plain', + ], + 'tmp_name' => [ + $file1, + $file2, + ], + 'error' => [ + 0, 0, + ], + 'size' => [ + null, null, + ], + ], + ]); + + /** @var UploadedFile[] */ + $files = $bag->get('child'); + + $this->assertEquals('test.txt', $files[0]->getClientOriginalName()); + $this->assertEquals('test.txt', $files[1]->getClientOriginalName()); + + $this->assertEquals('webkitdirectory/test.txt', $files[0]->getClientOriginalPath()); + $this->assertEquals('webkitdirectory/nested/test.txt', $files[1]->getClientOriginalPath()); + } + protected function createTempFile() { $tempFile = tempnam(sys_get_temp_dir().'/form_test', 'FormTest'); From 1b4a246e8db2bcdc8d730424e59e87f945d71a08 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Mon, 20 Nov 2023 19:43:22 +0100 Subject: [PATCH 016/395] [Form] Clean unused code --- .../NumberToLocalizedStringTransformer.php | 34 ++++------- .../PercentToLocalizedStringTransformer.php | 56 +++++++------------ src/Symfony/Component/Form/Form.php | 7 --- src/Symfony/Component/Form/FormBuilder.php | 5 -- 4 files changed, 29 insertions(+), 73 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php index 911246782df98..0693e79797599 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -183,35 +183,21 @@ protected function castParsedValue(int|float $value): int|float */ private function round(int|float $number): int|float { - if (null !== $this->scale && null !== $this->roundingMode) { + if (null !== $this->scale) { // shift number to maintain the correct scale during rounding $roundingCoef = 10 ** $this->scale; // string representation to avoid rounding errors, similar to bcmul() $number = (string) ($number * $roundingCoef); - switch ($this->roundingMode) { - case \NumberFormatter::ROUND_CEILING: - $number = ceil($number); - break; - case \NumberFormatter::ROUND_FLOOR: - $number = floor($number); - break; - case \NumberFormatter::ROUND_UP: - $number = $number > 0 ? ceil($number) : floor($number); - break; - case \NumberFormatter::ROUND_DOWN: - $number = $number > 0 ? floor($number) : ceil($number); - break; - case \NumberFormatter::ROUND_HALFEVEN: - $number = round($number, 0, \PHP_ROUND_HALF_EVEN); - break; - case \NumberFormatter::ROUND_HALFUP: - $number = round($number, 0, \PHP_ROUND_HALF_UP); - break; - case \NumberFormatter::ROUND_HALFDOWN: - $number = round($number, 0, \PHP_ROUND_HALF_DOWN); - break; - } + $number = match ($this->roundingMode) { + \NumberFormatter::ROUND_CEILING => ceil($number), + \NumberFormatter::ROUND_FLOOR => floor($number), + \NumberFormatter::ROUND_UP => $number > 0 ? ceil($number) : floor($number), + \NumberFormatter::ROUND_DOWN => $number > 0 ? floor($number) : ceil($number), + \NumberFormatter::ROUND_HALFEVEN => round($number, 0, \PHP_ROUND_HALF_EVEN), + \NumberFormatter::ROUND_HALFUP => round($number, 0, \PHP_ROUND_HALF_UP), + \NumberFormatter::ROUND_HALFDOWN => round($number, 0, \PHP_ROUND_HALF_DOWN), + }; $number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef; } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php index 0915021d3bab9..98d62783c1c00 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php @@ -180,9 +180,7 @@ protected function getNumberFormatter(): \NumberFormatter $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale); - if (null !== $this->roundingMode) { - $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode); - } + $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode); return $formatter; } @@ -192,43 +190,27 @@ protected function getNumberFormatter(): \NumberFormatter */ private function round(int|float $number): int|float { - if (null !== $this->scale && null !== $this->roundingMode) { - // shift number to maintain the correct scale during rounding - $roundingCoef = 10 ** $this->scale; + // shift number to maintain the correct scale during rounding + $roundingCoef = 10 ** $this->scale; - if (self::FRACTIONAL == $this->type) { - $roundingCoef *= 100; - } + if (self::FRACTIONAL === $this->type) { + $roundingCoef *= 100; + } - // string representation to avoid rounding errors, similar to bcmul() - $number = (string) ($number * $roundingCoef); - - switch ($this->roundingMode) { - case \NumberFormatter::ROUND_CEILING: - $number = ceil($number); - break; - case \NumberFormatter::ROUND_FLOOR: - $number = floor($number); - break; - case \NumberFormatter::ROUND_UP: - $number = $number > 0 ? ceil($number) : floor($number); - break; - case \NumberFormatter::ROUND_DOWN: - $number = $number > 0 ? floor($number) : ceil($number); - break; - case \NumberFormatter::ROUND_HALFEVEN: - $number = round($number, 0, \PHP_ROUND_HALF_EVEN); - break; - case \NumberFormatter::ROUND_HALFUP: - $number = round($number, 0, \PHP_ROUND_HALF_UP); - break; - case \NumberFormatter::ROUND_HALFDOWN: - $number = round($number, 0, \PHP_ROUND_HALF_DOWN); - break; - } + // string representation to avoid rounding errors, similar to bcmul() + $number = (string) ($number * $roundingCoef); - $number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef; - } + $number = match ($this->roundingMode) { + \NumberFormatter::ROUND_CEILING => ceil($number), + \NumberFormatter::ROUND_FLOOR => floor($number), + \NumberFormatter::ROUND_UP => $number > 0 ? ceil($number) : floor($number), + \NumberFormatter::ROUND_DOWN => $number > 0 ? floor($number) : ceil($number), + \NumberFormatter::ROUND_HALFEVEN => round($number, 0, \PHP_ROUND_HALF_EVEN), + \NumberFormatter::ROUND_HALFUP => round($number, 0, \PHP_ROUND_HALF_UP), + \NumberFormatter::ROUND_HALFDOWN => round($number, 0, \PHP_ROUND_HALF_DOWN), + }; + + $number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef; return $number; } diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 5c86d27b56f64..88c43766190a9 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -21,7 +21,6 @@ use Symfony\Component\Form\Exception\OutOfBoundsException; use Symfony\Component\Form\Exception\RuntimeException; use Symfony\Component\Form\Exception\TransformationFailedException; -use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Util\FormUtil; use Symfony\Component\Form\Util\InheritDataAwareIterator; @@ -727,12 +726,6 @@ public function add(FormInterface|string $child, string $type = null, array $opt } if (!$child instanceof FormInterface) { - if (!\is_string($child) && !\is_int($child)) { - throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormInterface'); - } - - $child = (string) $child; - // Never initialize child forms automatically $options['auto_initialize'] = false; diff --git a/src/Symfony/Component/Form/FormBuilder.php b/src/Symfony/Component/Form/FormBuilder.php index 33f07b0f1dc05..816c38810fc39 100644 --- a/src/Symfony/Component/Form/FormBuilder.php +++ b/src/Symfony/Component/Form/FormBuilder.php @@ -14,7 +14,6 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Exception\BadMethodCallException; use Symfony\Component\Form\Exception\InvalidArgumentException; -use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Extension\Core\Type\TextType; /** @@ -60,10 +59,6 @@ public function add(FormBuilderInterface|string $child, string $type = null, arr return $this; } - if (!\is_string($child) && !\is_int($child)) { - throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormBuilderInterface'); - } - // Add to "children" to maintain order $this->children[$child] = null; $this->unresolvedChildren[$child] = [$type, $options]; From 68673a3f9878cc5e38c374e8c23b28f67906247d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 18 Oct 2023 10:16:04 +0200 Subject: [PATCH 017/395] [HttpKernel] Introduce `ExceptionEvent::isKernelTerminating()` to skip error rendering when kernel is terminating --- UPGRADE-7.1.md | 7 +++++++ .../Component/HttpKernel/Event/ExceptionEvent.php | 9 ++++++++- .../HttpKernel/EventListener/ErrorListener.php | 4 ++++ src/Symfony/Component/HttpKernel/HttpKernel.php | 10 ++++++++-- .../Tests/EventListener/ErrorListenerTest.php | 13 +++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 UPGRADE-7.1.md diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md new file mode 100644 index 0000000000000..f5bdda37813e4 --- /dev/null +++ b/UPGRADE-7.1.md @@ -0,0 +1,7 @@ +UPGRADE FROM 7.0 to 7.1 +======================= + +HttpKernel +---------- + + * `ExceptionEvent` now takes an optional `$isKernelTerminating` parameter diff --git a/src/Symfony/Component/HttpKernel/Event/ExceptionEvent.php b/src/Symfony/Component/HttpKernel/Event/ExceptionEvent.php index 8bc25f9c37b81..84210b45c92c1 100644 --- a/src/Symfony/Component/HttpKernel/Event/ExceptionEvent.php +++ b/src/Symfony/Component/HttpKernel/Event/ExceptionEvent.php @@ -31,12 +31,14 @@ final class ExceptionEvent extends RequestEvent { private \Throwable $throwable; private bool $allowCustomResponseCode = false; + private bool $isKernelTerminating = false; - public function __construct(HttpKernelInterface $kernel, Request $request, int $requestType, \Throwable $e) + public function __construct(HttpKernelInterface $kernel, Request $request, int $requestType, \Throwable $e, bool $isKernelTerminating = false) { parent::__construct($kernel, $request, $requestType); $this->setThrowable($e); + $this->isKernelTerminating = $isKernelTerminating; } public function getThrowable(): \Throwable @@ -69,4 +71,9 @@ public function isAllowingCustomResponseCode(): bool { return $this->allowCustomResponseCode; } + + public function isKernelTerminating(): bool + { + return $this->isKernelTerminating; + } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php index a2f6db57a6e7f..4c4aafb9e36eb 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php @@ -102,6 +102,10 @@ public function onKernelException(ExceptionEvent $event) return; } + if (!$this->debug && $event->isKernelTerminating()) { + return; + } + $throwable = $event->getThrowable(); if ($exceptionHandler = set_exception_handler(var_dump(...))) { diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index d2cf4eaee27ce..693d6933330f4 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -56,6 +56,7 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface protected $requestStack; private ArgumentResolverInterface $argumentResolver; private bool $handleAllThrowables; + private bool $terminating = false; public function __construct(EventDispatcherInterface $dispatcher, ControllerResolverInterface $resolver, RequestStack $requestStack = null, ArgumentResolverInterface $argumentResolver = null, bool $handleAllThrowables = false) { @@ -112,7 +113,12 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R */ public function terminate(Request $request, Response $response) { - $this->dispatcher->dispatch(new TerminateEvent($this, $request, $response), KernelEvents::TERMINATE); + try { + $this->terminating = true; + $this->dispatcher->dispatch(new TerminateEvent($this, $request, $response), KernelEvents::TERMINATE); + } finally { + $this->terminating = false; + } } /** @@ -235,7 +241,7 @@ private function finishRequest(Request $request, int $type): void */ private function handleThrowable(\Throwable $e, Request $request, int $type): Response { - $event = new ExceptionEvent($this, $request, $type, $e); + $event = new ExceptionEvent($this, $request, $type, $e, isKernelTerminating: $this->terminating); $this->dispatcher->dispatch($event, KernelEvents::EXCEPTION); // a listener might have replaced the exception diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php index be98f1ceacb93..214178984c2ff 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php @@ -244,6 +244,19 @@ public function testCSPHeaderIsRemoved() $this->assertFalse($response->headers->has('content-security-policy'), 'CSP header has been removed'); } + public function testTerminating() + { + $listener = new ErrorListener('foo', $this->createMock(LoggerInterface::class)); + + $kernel = $this->createMock(HttpKernelInterface::class); + $kernel->expects($this->never())->method('handle'); + + $request = Request::create('/'); + + $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new \Exception('foo'), true); + $listener->onKernelException($event); + } + /** * @dataProvider controllerProvider */ From a206da9bdb25510300ab088d1ef087caa201726c Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 22 Nov 2023 11:46:42 +0100 Subject: [PATCH 018/395] [DependencyInjection] Fix tests on configuration prepend --- .../DependencyInjection/MergeExtensionConfigurationPassTest.php | 2 +- .../HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php index c22e05636ad71..ac2264f3b224a 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php @@ -48,7 +48,7 @@ public function testFooBundle() $configPass = new MergeExtensionConfigurationPass(['loaded', 'acme_foo']); $configPass->process($container); - $this->assertSame([[], ['bar' => 'baz']], $container->getExtensionConfig('loaded'), '->prependExtension() prepends an extension config'); + $this->assertSame([['bar' => 'baz'], []], $container->getExtensionConfig('loaded'), '->prependExtension() prepends an extension config'); $this->assertTrue($container->hasDefinition('acme_foo.foo'), '->loadExtension() registers a service'); $this->assertTrue($container->hasDefinition('acme_foo.bar'), '->loadExtension() imports a service'); $this->assertTrue($container->hasParameter('acme_foo.config'), '->loadExtension() sets a parameter'); diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php index 959536ecbdc51..ef75bdfa08384 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php @@ -31,7 +31,7 @@ public function configure(DefinitionConfigurator $definition): void public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void { - $container->extension('loaded', ['bar' => 'baz'], true); + $builder->prependExtensionConfig('loaded', ['bar' => 'baz']); } public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void From d77db34b12c09202351c0bced5b2c00e3edba73c Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 23 Nov 2023 23:54:13 +0100 Subject: [PATCH 019/395] document added method in the changelog rather than the upgrade files --- UPGRADE-7.1.md | 7 ------- src/Symfony/Component/HttpKernel/CHANGELOG.md | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 UPGRADE-7.1.md diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md deleted file mode 100644 index f5bdda37813e4..0000000000000 --- a/UPGRADE-7.1.md +++ /dev/null @@ -1,7 +0,0 @@ -UPGRADE FROM 7.0 to 7.1 -======================= - -HttpKernel ----------- - - * `ExceptionEvent` now takes an optional `$isKernelTerminating` parameter diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 4c6ed6428dfdd..eb0e3c1cb44e0 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add method `isKernelTerminating()` to `ExceptionEvent` that allows to check if an exception was thrown while the kernel is being terminated + 7.0 --- From 24cdc43bc154e767372a6df00ac70d856dd1ccd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4dlich?= Date: Sat, 25 Nov 2023 10:37:56 +0100 Subject: [PATCH 020/395] [Serializer] Consider SerializedPath in debug command output --- src/Symfony/Component/Serializer/Command/DebugCommand.php | 1 + .../Component/Serializer/Tests/Command/DebugCommandTest.php | 2 ++ src/Symfony/Component/Serializer/Tests/Dummy/DummyClassOne.php | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/Symfony/Component/Serializer/Command/DebugCommand.php b/src/Symfony/Component/Serializer/Command/DebugCommand.php index 13873dd1f9978..c85ee213e7f68 100644 --- a/src/Symfony/Component/Serializer/Command/DebugCommand.php +++ b/src/Symfony/Component/Serializer/Command/DebugCommand.php @@ -102,6 +102,7 @@ private function getAttributesData(ClassMetadataInterface $classMetadata): array 'groups' => $attributeMetadata->getGroups(), 'maxDepth' => $attributeMetadata->getMaxDepth(), 'serializedName' => $attributeMetadata->getSerializedName(), + 'serializedPath' => $attributeMetadata->getSerializedPath() ? (string) $attributeMetadata->getSerializedPath() : null, 'ignore' => $attributeMetadata->isIgnored(), 'normalizationContexts' => $attributeMetadata->getNormalizationContexts(), 'denormalizationContexts' => $attributeMetadata->getDenormalizationContexts(), diff --git a/src/Symfony/Component/Serializer/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Serializer/Tests/Command/DebugCommandTest.php index 879231160fe9d..5b4f73c1764f6 100644 --- a/src/Symfony/Component/Serializer/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Serializer/Tests/Command/DebugCommandTest.php @@ -46,6 +46,7 @@ public function testOutputWithClassArgument() | | ], | | | "maxDepth" => 1, | | | "serializedName" => "identifier", | + | | "serializedPath" => null, | | | "ignore" => true, | | | "normalizationContexts" => [ | | | "*" => [ | @@ -66,6 +67,7 @@ public function testOutputWithClassArgument() | | "groups" => [], | | | "maxDepth" => null, | | | "serializedName" => null, | + | | "serializedPath" => [data][name], | | | "ignore" => false, | | | "normalizationContexts" => [], | | | "denormalizationContexts" => [] | diff --git a/src/Symfony/Component/Serializer/Tests/Dummy/DummyClassOne.php b/src/Symfony/Component/Serializer/Tests/Dummy/DummyClassOne.php index 2b3c94cb8beae..fc78db51ce06e 100644 --- a/src/Symfony/Component/Serializer/Tests/Dummy/DummyClassOne.php +++ b/src/Symfony/Component/Serializer/Tests/Dummy/DummyClassOne.php @@ -16,6 +16,7 @@ use Symfony\Component\Serializer\Attribute\Ignore; use Symfony\Component\Serializer\Attribute\MaxDepth; use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Component\Serializer\Attribute\SerializedPath; class DummyClassOne { @@ -29,5 +30,6 @@ class DummyClassOne )] public string $code; + #[SerializedPath('[data][name]')] public string $name; } From bcecd7ed566bf7e07befb9201934bbf632097e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4dlich?= Date: Sun, 26 Nov 2023 20:01:11 +0100 Subject: [PATCH 021/395] Fix DebugCommandTest --- .../Tests/Command/DebugCommandTest.php | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/Symfony/Component/Serializer/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Serializer/Tests/Command/DebugCommandTest.php index 5b4f73c1764f6..7bfdf93ddd55c 100644 --- a/src/Symfony/Component/Serializer/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Serializer/Tests/Command/DebugCommandTest.php @@ -36,43 +36,43 @@ public function testOutputWithClassArgument() Symfony\Component\Serializer\Tests\Dummy\DummyClassOne ------------------------------------------------------ - +----------+-------------------------------------+ - | Property | Options | - +----------+-------------------------------------+ - | code | [ | - | | "groups" => [ | - | | "book:read", | - | | "book:write" | - | | ], | - | | "maxDepth" => 1, | - | | "serializedName" => "identifier", | - | | "serializedPath" => null, | - | | "ignore" => true, | - | | "normalizationContexts" => [ | - | | "*" => [ | - | | "groups" => [ | - | | "book:read" | - | | ] | - | | ] | - | | ], | - | | "denormalizationContexts" => [ | - | | "*" => [ | - | | "groups" => [ | - | | "book:write" | - | | ] | - | | ] | - | | ] | - | | ] | - | name | [ | - | | "groups" => [], | - | | "maxDepth" => null, | - | | "serializedName" => null, | - | | "serializedPath" => [data][name], | - | | "ignore" => false, | - | | "normalizationContexts" => [], | - | | "denormalizationContexts" => [] | - | | ] | - +----------+-------------------------------------+ + +----------+---------------------------------------+ + | Property | Options | + +----------+---------------------------------------+ + | code | [ | + | | "groups" => [ | + | | "book:read", | + | | "book:write" | + | | ], | + | | "maxDepth" => 1, | + | | "serializedName" => "identifier", | + | | "serializedPath" => null, | + | | "ignore" => true, | + | | "normalizationContexts" => [ | + | | "*" => [ | + | | "groups" => [ | + | | "book:read" | + | | ] | + | | ] | + | | ], | + | | "denormalizationContexts" => [ | + | | "*" => [ | + | | "groups" => [ | + | | "book:write" | + | | ] | + | | ] | + | | ] | + | | ] | + | name | [ | + | | "groups" => [], | + | | "maxDepth" => null, | + | | "serializedName" => null, | + | | "serializedPath" => "[data][name]", | + | | "ignore" => false, | + | | "normalizationContexts" => [], | + | | "denormalizationContexts" => [] | + | | ] | + +----------+---------------------------------------+ TXT, $tester->getDisplay(true), From d8e48aca6c676d4673659e2d116ccd588ad3539c Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 29 Nov 2023 13:43:16 +0100 Subject: [PATCH 022/395] remove useless setAccessible() calls --- .../Component/DependencyInjection/Tests/ContainerTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php index ccec9839e4e9f..2a9b822d0a681 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php @@ -420,7 +420,6 @@ public function testGetEnvDoesNotAutoCastNullWithDefaultEnvVarProcessor() $container->compile(); $r = new \ReflectionMethod($container, 'getEnv'); - $r->setAccessible(true); $this->assertNull($r->invoke($container, 'FOO')); } @@ -436,7 +435,6 @@ public function testGetEnvDoesNotAutoCastNullWithEnvVarProcessorsLocatorReturnin $container->compile(); $r = new \ReflectionMethod($container, 'getEnv'); - $r->setAccessible(true); $this->assertNull($r->invoke($container, 'FOO')); } } From b26386c88a5a3e1c024f847e8a2a90080e8410a2 Mon Sep 17 00:00:00 2001 From: Tac Tacelosky Date: Sat, 2 Dec 2023 07:52:40 -0500 Subject: [PATCH 023/395] remove $ so gitclip works --- src/Symfony/Component/Translation/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Translation/README.md b/src/Symfony/Component/Translation/README.md index 32e4017b72ed3..0edcae654ecaa 100644 --- a/src/Symfony/Component/Translation/README.md +++ b/src/Symfony/Component/Translation/README.md @@ -6,8 +6,8 @@ The Translation component provides tools to internationalize your application. Getting Started --------------- -``` -$ composer require symfony/translation +```bash +composer require symfony/translation ``` ```php From 19abb993dbbaf63a55213596e200dbeae726fdff Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 1 Dec 2023 16:42:06 +0100 Subject: [PATCH 024/395] [Workflow] Add `getEnabledTransition()` method annotation to WorkflowInterface --- UPGRADE-7.1.md | 7 +++++++ src/Symfony/Component/Workflow/CHANGELOG.md | 5 +++++ src/Symfony/Component/Workflow/WorkflowInterface.php | 2 ++ 3 files changed, 14 insertions(+) create mode 100644 UPGRADE-7.1.md diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md new file mode 100644 index 0000000000000..c0848e61e651e --- /dev/null +++ b/UPGRADE-7.1.md @@ -0,0 +1,7 @@ +UPGRADE FROM 7.0 to 7.1 +======================= + +Workflow +-------- + + * Add method `getEnabledTransition()` to `WorkflowInterface` diff --git a/src/Symfony/Component/Workflow/CHANGELOG.md b/src/Symfony/Component/Workflow/CHANGELOG.md index 00840acee36c1..f8b83a59a9689 100644 --- a/src/Symfony/Component/Workflow/CHANGELOG.md +++ b/src/Symfony/Component/Workflow/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add method `getEnabledTransition()` to `WorkflowInterface` + 7.0 --- diff --git a/src/Symfony/Component/Workflow/WorkflowInterface.php b/src/Symfony/Component/Workflow/WorkflowInterface.php index 17aa7e04d5fd0..8e0faef081788 100644 --- a/src/Symfony/Component/Workflow/WorkflowInterface.php +++ b/src/Symfony/Component/Workflow/WorkflowInterface.php @@ -19,6 +19,8 @@ * Describes a workflow instance. * * @author Amrouche Hamza + * + * @method Transition|null getEnabledTransition(object $subject, string $name) */ interface WorkflowInterface { From 3ea601d2e677dd11cb265496d54a597e72c1bd6d Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Mon, 20 Nov 2023 19:32:45 +0100 Subject: [PATCH 025/395] [CssSelector][Serializer][Translation] [Command] Clean unused code --- src/Symfony/Component/Console/Command/Command.php | 4 ---- src/Symfony/Component/CssSelector/XPath/XPathExpr.php | 2 +- src/Symfony/Component/Serializer/Encoder/XmlEncoder.php | 8 ++------ .../Translation/Bridge/Crowdin/CrowdinProvider.php | 4 ---- .../Component/Translation/Bridge/Loco/LocoProvider.php | 8 -------- .../Translation/Bridge/Lokalise/LokaliseProvider.php | 8 -------- .../Translation/Command/TranslationPushCommand.php | 2 +- .../Component/Translation/Formatter/MessageFormatter.php | 6 +----- 8 files changed, 5 insertions(+), 37 deletions(-) diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index c49891777af7d..ef1e7c31e9087 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -277,10 +277,6 @@ public function run(InputInterface $input, OutputInterface $output): int $statusCode = ($this->code)($input, $output); } else { $statusCode = $this->execute($input, $output); - - if (!\is_int($statusCode)) { - throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode))); - } } return is_numeric($statusCode) ? (int) $statusCode : 0; diff --git a/src/Symfony/Component/CssSelector/XPath/XPathExpr.php b/src/Symfony/Component/CssSelector/XPath/XPathExpr.php index a76e30bec37d2..29f0c8a406a5a 100644 --- a/src/Symfony/Component/CssSelector/XPath/XPathExpr.php +++ b/src/Symfony/Component/CssSelector/XPath/XPathExpr.php @@ -104,7 +104,7 @@ public function join(string $combiner, self $expr): static public function __toString(): string { $path = $this->path.$this->element; - $condition = null === $this->condition || '' === $this->condition ? '' : '['.$this->condition.']'; + $condition = '' === $this->condition ? '' : '['.$this->condition.']'; return $path.$condition; } diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index 24d786e38bee0..a3809bc84c05b 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -200,13 +200,9 @@ final protected function appendCData(\DOMNode $node, string $val): bool final protected function appendDocumentFragment(\DOMNode $node, \DOMDocumentFragment $fragment): bool { - if ($fragment instanceof \DOMDocumentFragment) { - $node->appendChild($fragment); + $node->appendChild($fragment); - return true; - } - - return false; + return true; } final protected function appendComment(\DOMNode $node, string $data): bool diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php index 23113bd237b74..96faf44b0a3e4 100644 --- a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php @@ -176,10 +176,6 @@ public function delete(TranslatorBagInterface $translatorBag): void $defaultCatalogue = $translatorBag->getCatalogue($this->defaultLocale); - if (!$defaultCatalogue) { - $defaultCatalogue = $translatorBag->getCatalogues()[0]; - } - foreach ($defaultCatalogue->all() as $domain => $messages) { $fileId = $this->getFileIdByDomain($fileList, $domain); diff --git a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php index f43a825eb8993..c3b6d2267a6fe 100644 --- a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php @@ -57,10 +57,6 @@ public function write(TranslatorBagInterface $translatorBag): void { $catalogue = $translatorBag->getCatalogue($this->defaultLocale); - if (!$catalogue) { - $catalogue = $translatorBag->getCatalogues()[0]; - } - foreach ($catalogue->all() as $domain => $messages) { $createdIds = $this->createAssets(array_keys($messages), $domain); if ($createdIds) { @@ -175,10 +171,6 @@ public function delete(TranslatorBagInterface $translatorBag): void { $catalogue = $translatorBag->getCatalogue($this->defaultLocale); - if (!$catalogue) { - $catalogue = $translatorBag->getCatalogues()[0]; - } - $responses = []; foreach (array_keys($catalogue->all()) as $domain) { diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php index e4f9b20cf1722..6f4ff963cad5e 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php @@ -61,10 +61,6 @@ public function write(TranslatorBagInterface $translatorBag): void { $defaultCatalogue = $translatorBag->getCatalogue($this->defaultLocale); - if (!$defaultCatalogue) { - $defaultCatalogue = $translatorBag->getCatalogues()[0]; - } - $this->ensureAllLocalesAreCreated($translatorBag); $existingKeysByDomain = []; @@ -111,10 +107,6 @@ public function delete(TranslatorBagInterface $translatorBag): void { $catalogue = $translatorBag->getCatalogue($this->defaultLocale); - if (!$catalogue) { - $catalogue = $translatorBag->getCatalogues()[0]; - } - $keysIds = []; foreach ($catalogue->getDomains() as $domain) { diff --git a/src/Symfony/Component/Translation/Command/TranslationPushCommand.php b/src/Symfony/Component/Translation/Command/TranslationPushCommand.php index 1d04adbc9d15e..3310ac6975361 100644 --- a/src/Symfony/Component/Translation/Command/TranslationPushCommand.php +++ b/src/Symfony/Component/Translation/Command/TranslationPushCommand.php @@ -60,7 +60,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti if ($input->mustSuggestOptionValuesFor('domains')) { $provider = $this->providers->get($input->getArgument('provider')); - if ($provider && method_exists($provider, 'getDomains')) { + if (method_exists($provider, 'getDomains')) { $domains = $provider->getDomains(); $suggestions->suggestValues($domains); } diff --git a/src/Symfony/Component/Translation/Formatter/MessageFormatter.php b/src/Symfony/Component/Translation/Formatter/MessageFormatter.php index 29ad574ee12e3..5e101aa438c19 100644 --- a/src/Symfony/Component/Translation/Formatter/MessageFormatter.php +++ b/src/Symfony/Component/Translation/Formatter/MessageFormatter.php @@ -36,11 +36,7 @@ public function __construct(TranslatorInterface $translator = null, IntlFormatte public function format(string $message, string $locale, array $parameters = []): string { - if ($this->translator instanceof TranslatorInterface) { - return $this->translator->trans($message, $parameters, null, $locale); - } - - return strtr($message, $parameters); + return $this->translator->trans($message, $parameters, null, $locale); } public function formatIntl(string $message, string $locale, array $parameters = []): string From 95f8c1db04b3cc96d5f175b449d7fdaaf25a4d79 Mon Sep 17 00:00:00 2001 From: Nyholm Date: Fri, 17 Nov 2023 15:33:13 +0100 Subject: [PATCH 026/395] [PropertyInfo] Make `PhpDocExtractor::getDocBlock()` public --- .../Component/PropertyInfo/CHANGELOG.md | 5 +++ .../Extractor/PhpDocExtractor.php | 18 +++++++--- .../PropertyDocBlockExtractorInterface.php | 36 +++++++++++++++++++ .../Tests/Extractor/PhpDocExtractorTest.php | 24 ++++++++++--- .../Extractor/ReflectionExtractorTest.php | 3 ++ .../PropertyInfo/Tests/Fixtures/Dummy.php | 2 ++ 6 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 src/Symfony/Component/PropertyInfo/PropertyDocBlockExtractorInterface.php diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index ce7f220ce1dc1..6e0a2ff449dec 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Introduce `PropertyDocBlockExtractorInterface` to extract a property's doc block + 6.4 --- diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php index e329e9c0d2804..ab056e12b0eba 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -18,6 +18,7 @@ use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\ContextFactory; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyDocBlockExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper; @@ -29,7 +30,7 @@ * * @final */ -class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface +class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface, PropertyDocBlockExtractorInterface { public const PROPERTY = 0; public const ACCESSOR = 1; @@ -74,7 +75,7 @@ public function __construct(DocBlockFactoryInterface $docBlockFactory = null, ar public function getShortDescription(string $class, string $property, array $context = []): ?string { /** @var $docBlock DocBlock */ - [$docBlock] = $this->getDocBlock($class, $property); + [$docBlock] = $this->findDocBlock($class, $property); if (!$docBlock) { return null; } @@ -101,7 +102,7 @@ public function getShortDescription(string $class, string $property, array $cont public function getLongDescription(string $class, string $property, array $context = []): ?string { /** @var $docBlock DocBlock */ - [$docBlock] = $this->getDocBlock($class, $property); + [$docBlock] = $this->findDocBlock($class, $property); if (!$docBlock) { return null; } @@ -114,7 +115,7 @@ public function getLongDescription(string $class, string $property, array $conte public function getTypes(string $class, string $property, array $context = []): ?array { /** @var $docBlock DocBlock */ - [$docBlock, $source, $prefix] = $this->getDocBlock($class, $property); + [$docBlock, $source, $prefix] = $this->findDocBlock($class, $property); if (!$docBlock) { return null; } @@ -187,6 +188,13 @@ public function getTypesFromConstructor(string $class, string $property): ?array return array_merge([], ...$types); } + public function getDocBlock(string $class, string $property): ?DocBlock + { + $output = $this->findDocBlock($class, $property); + + return $output[0]; + } + private function getDocBlockFromConstructor(string $class, string $property): ?DocBlock { try { @@ -219,7 +227,7 @@ private function filterDocBlockParams(DocBlock $docBlock, string $allowedParam): /** * @return array{DocBlock|null, int|null, string|null} */ - private function getDocBlock(string $class, string $property): array + private function findDocBlock(string $class, string $property): array { $propertyHash = sprintf('%s::%s', $class, $property); diff --git a/src/Symfony/Component/PropertyInfo/PropertyDocBlockExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyDocBlockExtractorInterface.php new file mode 100644 index 0000000000000..4a51d7b79cfb5 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyDocBlockExtractorInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +use phpDocumentor\Reflection\DocBlock; + +/** + * Extract a property's doc block. + * + * A property's doc block may be located on a constructor promoted argument, on + * the property or on a mutator for that property. + * + * @author Tobias Nyholm + */ +interface PropertyDocBlockExtractorInterface +{ + /** + * Gets the first available doc block for a property. It finds the doc block + * by the following priority: + * - constructor promoted argument + * - the class property + * - a mutator method for that property + * + * If no doc block is found, it will return null. + */ + public function getDocBlock(string $class, string $property): ?DocBlock; +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index ecb9f57079a1a..db39180b0a899 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\PropertyInfo\Tests\Extractor; +use phpDocumentor\Reflection\DocBlock; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; @@ -38,9 +39,22 @@ protected function setUp(): void */ public function testExtract($property, ?array $type, $shortDescription, $longDescription) { - $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); - $this->assertSame($shortDescription, $this->extractor->getShortDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); - $this->assertSame($longDescription, $this->extractor->getLongDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); + $this->assertEquals($type, $this->extractor->getTypes(Dummy::class, $property)); + $this->assertSame($shortDescription, $this->extractor->getShortDescription(Dummy::class, $property)); + $this->assertSame($longDescription, $this->extractor->getLongDescription(Dummy::class, $property)); + } + + public function testGetDocBlock() + { + $docBlock = $this->extractor->getDocBlock(Dummy::class, 'g'); + $this->assertInstanceOf(DocBlock::class, $docBlock); + $this->assertSame('Nullable array.', $docBlock->getSummary()); + + $docBlock = $this->extractor->getDocBlock(Dummy::class, 'noDocBlock;'); + $this->assertNull($docBlock); + + $docBlock = $this->extractor->getDocBlock(Dummy::class, 'notAvailable'); + $this->assertNull($docBlock); } public function testParamTagTypeIsOmitted() @@ -75,7 +89,7 @@ public function testExtractTypesWithNoPrefixes($property, array $type = null) { $noPrefixExtractor = new PhpDocExtractor(null, [], [], []); - $this->assertEquals($type, $noPrefixExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); + $this->assertEquals($type, $noPrefixExtractor->getTypes(Dummy::class, $property)); } public static function typesProvider() @@ -197,7 +211,7 @@ public function testExtractTypesWithCustomPrefixes($property, array $type = null { $customExtractor = new PhpDocExtractor(null, ['add', 'remove'], ['is', 'can']); - $this->assertEquals($type, $customExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); + $this->assertEquals($type, $customExtractor->getTypes(Dummy::class, $property)); } public static function typesWithCustomPrefixesProvider() diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 39bcec722c752..1a312921671e3 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -66,6 +66,7 @@ public function testGetProperties() 'arrayWithKeys', 'arrayWithKeysAndComplexValue', 'arrayOfMixed', + 'noDocBlock', 'listOfStrings', 'parentAnnotation', 'foo', @@ -130,6 +131,7 @@ public function testGetPropertiesWithCustomPrefixes() 'arrayWithKeys', 'arrayWithKeysAndComplexValue', 'arrayOfMixed', + 'noDocBlock', 'listOfStrings', 'parentAnnotation', 'foo', @@ -183,6 +185,7 @@ public function testGetPropertiesWithNoPrefixes() 'arrayWithKeys', 'arrayWithKeysAndComplexValue', 'arrayOfMixed', + 'noDocBlock', 'listOfStrings', 'parentAnnotation', 'foo', diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index ec3bb8da4e200..68b2e1d9928cb 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -155,6 +155,8 @@ class Dummy extends ParentDummy */ public $arrayOfMixed; + public $noDocBlock; + /** * @var list */ From ae454e0648b23be545a35b2c30a7096c496e1a81 Mon Sep 17 00:00:00 2001 From: javaDeveloperKid Date: Wed, 1 Nov 2023 18:42:48 +0100 Subject: [PATCH 027/395] [Messenger] Add `--all` option to `messenger:consume` --- src/Symfony/Component/Messenger/CHANGELOG.md | 5 +++ .../Command/ConsumeMessagesCommand.php | 23 ++++++++++-- .../Command/ConsumeMessagesCommandTest.php | 36 +++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index 6be9bee7fadcf..1329387596e62 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `--all` option to the `messenger:consume` command + 7.0 --- diff --git a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php index 28ffee1c37752..129995de7b30b 100644 --- a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php +++ b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php @@ -34,6 +34,7 @@ use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener; use Symfony\Component\Messenger\EventListener\StopWorkerOnTimeLimitListener; use Symfony\Component\Messenger\RoutableMessageBus; +use Symfony\Component\Messenger\Transport\Sync\SyncTransport; use Symfony\Component\Messenger\Worker; /** @@ -83,6 +84,7 @@ protected function configure(): void new InputOption('bus', 'b', InputOption::VALUE_REQUIRED, 'Name of the bus to which received messages should be dispatched (if not passed, bus is determined automatically)'), new InputOption('queues', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Limit receivers to only consume from the specified queues'), new InputOption('no-reset', null, InputOption::VALUE_NONE, 'Do not reset container services after each message'), + new InputOption('all', null, InputOption::VALUE_NONE, 'Consume messages from all receivers'), ]) ->setHelp(<<<'EOF' The %command.name% command consumes messages and dispatches them to the message bus. @@ -123,6 +125,10 @@ protected function configure(): void Use the --no-reset option to prevent services resetting after each message (may lead to leaking services' state between messages): php %command.full_name% --no-reset + +Use the --all option to consume from all receivers: + + php %command.full_name% --all EOF ) ; @@ -132,6 +138,10 @@ protected function interact(InputInterface $input, OutputInterface $output): voi { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + if ($input->getOption('all')) { + return; + } + if ($this->receiverNames && !$input->getArgument('receivers')) { $io->block('Which transports/receivers do you want to consume?', null, 'fg=white;bg=blue', ' ', true); @@ -155,7 +165,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $receivers = []; $rateLimiters = []; - foreach ($receiverNames = $input->getArgument('receivers') as $receiverName) { + $receiverNames = $input->getOption('all') ? $this->receiverNames : $input->getArgument('receivers'); + foreach ($receiverNames as $receiverName) { if (!$this->receiverLocator->has($receiverName)) { $message = sprintf('The receiver "%s" does not exist.', $receiverName); if ($this->receiverNames) { @@ -165,7 +176,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new RuntimeException($message); } - $receivers[$receiverName] = $this->receiverLocator->get($receiverName); + $receiver = $this->receiverLocator->get($receiverName); + if ($receiver instanceof SyncTransport) { + $idx = array_search($receiverName, $receiverNames); + unset($receiverNames[$idx]); + + continue; + } + + $receivers[$receiverName] = $receiver; if ($this->rateLimiterLocator?->has($receiverName)) { $rateLimiters[$receiverName] = $this->rateLimiterLocator->get($receiverName); } diff --git a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php index 0173052290047..40579ece6fa21 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php @@ -214,6 +214,42 @@ public function testRunWithTimeLimit() $this->assertStringContainsString('[OK] Consuming messages from transport "dummy-receiver"', $tester->getDisplay()); } + public function testRunWithAllOption() + { + $envelope1 = new Envelope(new \stdClass(), [new BusNameStamp('dummy-bus')]); + $envelope2 = new Envelope(new \stdClass(), [new BusNameStamp('dummy-bus')]); + + $receiver1 = $this->createMock(ReceiverInterface::class); + $receiver1->expects($this->once())->method('get')->willReturn([$envelope1]); + $receiver2 = $this->createMock(ReceiverInterface::class); + $receiver2->expects($this->once())->method('get')->willReturn([$envelope2]); + + $receiverLocator = $this->createMock(ContainerInterface::class); + $receiverLocator->expects($this->once())->method('has')->with('dummy-receiver1')->willReturn(true); + $receiverLocator->expects($this->once())->method('get')->with('dummy-receiver1')->willReturn($receiver1); + $receiverLocator->expects($this->once())->method('has')->with('dummy-receiver2')->willReturn(true); + $receiverLocator->expects($this->once())->method('get')->with('dummy-receiver2')->willReturn($receiver2); + + $bus = $this->createMock(MessageBusInterface::class); + $bus->expects($this->exactly(2))->method('dispatch'); + + $busLocator = $this->createMock(ContainerInterface::class); + $busLocator->expects($this->once())->method('has')->with('dummy-bus')->willReturn(true); + $busLocator->expects($this->once())->method('get')->with('dummy-bus')->willReturn($bus); + + $command = new ConsumeMessagesCommand(new RoutableMessageBus($busLocator), $receiverLocator, new EventDispatcher()); + + $application = new Application(); + $application->add($command); + $tester = new CommandTester($application->get('messenger:consume')); + $tester->execute([ + '--all' => null, + ]); + + $tester->assertCommandIsSuccessful(); + $this->assertStringContainsString('[OK] Consuming messages from transport "dummy-receiver1, dummy-receiver2"', $tester->getDisplay()); + } + /** * @dataProvider provideCompletionSuggestions */ From 7c1bfcf0ae3562012954c0d04b42828a050f27f0 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Dec 2023 22:20:56 +0100 Subject: [PATCH 028/395] make both options redis_sentinel and sentinel_master available everywhere --- src/Symfony/Component/Cache/CHANGELOG.md | 5 +++++ src/Symfony/Component/Cache/Traits/RedisTrait.php | 6 ++++++ .../Tests/Transport/RedisExtIntegrationTest.php | 13 +++++++++++-- .../Messenger/Bridge/Redis/Transport/Connection.php | 7 ++++++- src/Symfony/Component/Messenger/CHANGELOG.md | 1 + 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index 3290ae2e38aeb..69e8efb63483e 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add option `sentinel_master` as an alias for `redis_sentinel` + 7.0 --- diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index 4928db07f4472..9852484288dc8 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -169,6 +169,12 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra $params += $query + $options + self::$defaultConnectionOptions; + if (isset($params['redis_sentinel']) && isset($params['sentinel_master'])) { + throw new InvalidArgumentException('Cannot use both "redis_sentinel" and "sentinel_master" at the same time.'); + } + + $params['redis_sentinel'] ??= $params['sentinel_master'] ?? null; + if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) { throw new CacheException('Redis Sentinel support requires one of: "predis/predis", "ext-redis >= 5.2", "ext-relay".'); } diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php index a80aecd32ecb2..e9b9e80062657 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php @@ -220,7 +220,10 @@ public function testConnectionClaimAndRedeliver() $connection->ack($message['id']); } - public function testSentinel() + /** + * @dataProvider + */ + public function testSentinel(string $sentinelOptionName) { if (!$hosts = getenv('REDIS_SENTINEL_HOSTS')) { $this->markTestSkipped('REDIS_SENTINEL_HOSTS env var is not defined.'); @@ -234,7 +237,7 @@ public function testSentinel() $connection = Connection::fromDsn($dsn, ['delete_after_ack' => true, - 'sentinel_master' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null, + $sentinelOptionName => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null, ], $this->redis); $connection->add('1', []); @@ -249,6 +252,12 @@ public function testSentinel() $connection->cleanup(); } + public function sentinelOptionNames(): iterable + { + yield 'redis_sentinel'; + yield 'sentinel_master'; + } + public function testLazySentinel() { $connection = Connection::fromDsn(getenv('MESSENGER_REDIS_DSN'), diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php index bfdf13b8c119a..471f88375dfd3 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php @@ -78,7 +78,12 @@ public function __construct(array $options, \Redis|Relay|\RedisCluster $redis = $host = $options['host']; $port = $options['port']; $auth = $options['auth']; - $sentinelMaster = $options['sentinel_master']; + + if (isset($options['redis_sentinel']) && isset($options['sentinel_master'])) { + throw new InvalidArgumentException('Cannot use both "redis_sentinel" and "sentinel_master" at the same time.'); + } + + $sentinelMaster = $options['sentinel_master'] ?? $options['redis_sentinel'] ?? null; if (null !== $sentinelMaster && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) { throw new InvalidArgumentException('Redis Sentinel support requires ext-redis>=5.2, or ext-relay.'); diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index 1329387596e62..937a9fcb4dd8d 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.1 --- + * Add option `redis_sentinel` as an alias for `sentinel_master` * Add `--all` option to the `messenger:consume` command 7.0 From 7ace6211d5933907e9fec4030f4a6e79505430d3 Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Tue, 28 Nov 2023 17:09:16 +0100 Subject: [PATCH 029/395] [HttpClient] Allow mocking start_time info in MockResponse --- src/Symfony/Component/HttpClient/CHANGELOG.md | 5 +++++ .../Component/HttpClient/Response/MockResponse.php | 3 +++ .../Component/HttpClient/Tests/MockHttpClientTest.php | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md index 961f09cf42124..c9417a88315e7 100644 --- a/src/Symfony/Component/HttpClient/CHANGELOG.md +++ b/src/Symfony/Component/HttpClient/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Allow mocking `start_time` info in `MockResponse` + 7.0 --- diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index dba6307f2b5d9..ed2b2008f0c99 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -217,6 +217,9 @@ private static function writeRequest(self $response, array $options, ResponseInt { $onProgress = $options['on_progress'] ?? static function () {}; $response->info += $mock->getInfo() ?: []; + if (null !== $mock->getInfo('start_time')) { + $response->info['start_time'] = $mock->getInfo('start_time'); + } // simulate "size_upload" if it is set if (isset($response->info['size_upload'])) { diff --git a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php index 6da3af6bca9dd..26b516a42d314 100644 --- a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php @@ -573,4 +573,14 @@ public function testMoreRequestsThanResponseFactoryResponses() $client->request('GET', 'https://example.com'); $client->request('GET', 'https://example.com'); } + + public function testMockStartTimeInfo() + { + $client = new MockHttpClient(new MockResponse('foobarccc', [ + 'start_time' => 1701187598.313123, + ])); + + $response = $client->request('GET', 'https://example.com'); + $this->assertSame(1701187598.313123, $response->getInfo('start_time')); + } } From 36307a042a59318ef2c80982b26a17590c0e9388 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 5 Dec 2023 10:51:36 +0100 Subject: [PATCH 030/395] [Messenger] Fix test on `messenger:consume` with `--all` option --- .../Command/ConsumeMessagesCommandTest.php | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php index 40579ece6fa21..b7f33f2fcbd25 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php @@ -220,34 +220,42 @@ public function testRunWithAllOption() $envelope2 = new Envelope(new \stdClass(), [new BusNameStamp('dummy-bus')]); $receiver1 = $this->createMock(ReceiverInterface::class); - $receiver1->expects($this->once())->method('get')->willReturn([$envelope1]); + $receiver1->method('get')->willReturn([$envelope1]); $receiver2 = $this->createMock(ReceiverInterface::class); - $receiver2->expects($this->once())->method('get')->willReturn([$envelope2]); + $receiver2->method('get')->willReturn([$envelope2]); $receiverLocator = $this->createMock(ContainerInterface::class); - $receiverLocator->expects($this->once())->method('has')->with('dummy-receiver1')->willReturn(true); - $receiverLocator->expects($this->once())->method('get')->with('dummy-receiver1')->willReturn($receiver1); - $receiverLocator->expects($this->once())->method('has')->with('dummy-receiver2')->willReturn(true); - $receiverLocator->expects($this->once())->method('get')->with('dummy-receiver2')->willReturn($receiver2); + $receiverLocator->expects($this->exactly(2)) + ->method('has') + ->willReturnCallback(static fn (string $id): bool => \in_array($id, ['dummy-receiver1', 'dummy-receiver2'], true)); + + $receiverLocator->expects($this->exactly(2)) + ->method('get') + ->willReturnCallback(static fn (string $id): ReceiverInterface => 'dummy-receiver1' === $id ? $receiver1 : $receiver2); $bus = $this->createMock(MessageBusInterface::class); $bus->expects($this->exactly(2))->method('dispatch'); $busLocator = $this->createMock(ContainerInterface::class); - $busLocator->expects($this->once())->method('has')->with('dummy-bus')->willReturn(true); - $busLocator->expects($this->once())->method('get')->with('dummy-bus')->willReturn($bus); + $busLocator->expects($this->exactly(2))->method('has')->with('dummy-bus')->willReturn(true); + $busLocator->expects($this->exactly(2))->method('get')->with('dummy-bus')->willReturn($bus); - $command = new ConsumeMessagesCommand(new RoutableMessageBus($busLocator), $receiverLocator, new EventDispatcher()); + $command = new ConsumeMessagesCommand( + new RoutableMessageBus($busLocator), + $receiverLocator, new EventDispatcher(), + receiverNames: ['dummy-receiver1', 'dummy-receiver2'] + ); $application = new Application(); $application->add($command); $tester = new CommandTester($application->get('messenger:consume')); $tester->execute([ - '--all' => null, + '--all' => true, + '--limit' => 2, ]); $tester->assertCommandIsSuccessful(); - $this->assertStringContainsString('[OK] Consuming messages from transport "dummy-receiver1, dummy-receiver2"', $tester->getDisplay()); + $this->assertStringContainsString('[OK] Consuming messages from transports "dummy-receiver1, dummy-receiver2"', $tester->getDisplay()); } /** From 448c2b14cdb509f1bbd8e68522a80bfe401139a5 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 9 Aug 2023 15:43:20 +0200 Subject: [PATCH 031/395] [HttpFoundation] Add `QueryParameterRequestMatcher` --- .../Component/HttpFoundation/CHANGELOG.md | 1 + .../QueryParameterRequestMatcher.php | 46 +++++++++++++ .../QueryParameterRequestMatcherTest.php | 67 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 src/Symfony/Component/HttpFoundation/RequestMatcher/QueryParameterRequestMatcher.php create mode 100644 src/Symfony/Component/HttpFoundation/Tests/RequestMatcher/QueryParameterRequestMatcherTest.php diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index d4d07411f70e7..c3f62a9267f35 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `UploadedFile::getClientOriginalPath()` + * Add `QueryParameterRequestMatcher` 7.0 --- diff --git a/src/Symfony/Component/HttpFoundation/RequestMatcher/QueryParameterRequestMatcher.php b/src/Symfony/Component/HttpFoundation/RequestMatcher/QueryParameterRequestMatcher.php new file mode 100644 index 0000000000000..86161e7c031dc --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/RequestMatcher/QueryParameterRequestMatcher.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the presence of HTTP query parameters of a Request. + * + * @author Alexandre Daubois + */ +class QueryParameterRequestMatcher implements RequestMatcherInterface +{ + /** + * @var string[] + */ + private array $parameters; + + /** + * @param string[]|string $parameters A parameter or a list of parameters + * Strings can contain a comma-delimited list of query parameters + */ + public function __construct(array|string $parameters) + { + $this->parameters = array_reduce(array_map(strtolower(...), (array) $parameters), static fn (array $parameters, string $parameter) => array_merge($parameters, preg_split('/\s*,\s*/', $parameter)), []); + } + + public function matches(Request $request): bool + { + if (!$this->parameters) { + return true; + } + + return 0 === \count(array_diff_assoc($this->parameters, $request->query->keys())); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestMatcher/QueryParameterRequestMatcherTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestMatcher/QueryParameterRequestMatcherTest.php new file mode 100644 index 0000000000000..202ca649ab05f --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestMatcher/QueryParameterRequestMatcherTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\RequestMatcher; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcher\QueryParameterRequestMatcher; + +class QueryParameterRequestMatcherTest extends TestCase +{ + /** + * @dataProvider getDataForArray + */ + public function testArray(string $uri, bool $matches) + { + $matcher = new QueryParameterRequestMatcher(['foo', 'bar']); + $request = Request::create($uri); + $this->assertSame($matches, $matcher->matches($request)); + } + + /** + * @dataProvider getDataForArray + */ + public function testCommaSeparatedString(string $uri, bool $matches) + { + $matcher = new QueryParameterRequestMatcher('foo, bar'); + $request = Request::create($uri); + $this->assertSame($matches, $matcher->matches($request)); + } + + /** + * @dataProvider getDataForSingleString + */ + public function testSingleString(string $uri, bool $matches) + { + $matcher = new QueryParameterRequestMatcher('foo'); + $request = Request::create($uri); + $this->assertSame($matches, $matcher->matches($request)); + } + + public static function getDataForArray(): \Generator + { + yield ['https://example.com?foo=&bar=', true]; + yield ['https://example.com?foo=foo1&bar=bar1', true]; + yield ['https://example.com?foo=foo1&bar=bar1&baz=baz1', true]; + yield ['https://example.com?foo=', false]; + yield ['https://example.com', false]; + } + + public static function getDataForSingleString(): \Generator + { + yield ['https://example.com?foo=&bar=', true]; + yield ['https://example.com?foo=foo1', true]; + yield ['https://example.com?foo=', true]; + yield ['https://example.com?bar=bar1&baz=baz1', false]; + yield ['https://example.com', false]; + } +} From 70a74b78adf73052d2efe30018e0c731b37698fb Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 6 Dec 2023 16:49:39 +0100 Subject: [PATCH 032/395] [Messenger] Fix missing `@throws` phpdoc on MiddlewareInterface, MessageBusInterface and SenderInterface --- src/Symfony/Component/Messenger/MessageBusInterface.php | 3 +++ .../Component/Messenger/Middleware/MiddlewareInterface.php | 4 ++++ .../Component/Messenger/Transport/Sender/SenderInterface.php | 3 +++ 3 files changed, 10 insertions(+) diff --git a/src/Symfony/Component/Messenger/MessageBusInterface.php b/src/Symfony/Component/Messenger/MessageBusInterface.php index f1dbe0ad3800c..0cde1f6e516d2 100644 --- a/src/Symfony/Component/Messenger/MessageBusInterface.php +++ b/src/Symfony/Component/Messenger/MessageBusInterface.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Messenger; +use Symfony\Component\Messenger\Exception\ExceptionInterface; use Symfony\Component\Messenger\Stamp\StampInterface; /** @@ -23,6 +24,8 @@ interface MessageBusInterface * * @param object|Envelope $message The message or the message pre-wrapped in an envelope * @param StampInterface[] $stamps + * + * @throws ExceptionInterface */ public function dispatch(object $message, array $stamps = []): Envelope; } diff --git a/src/Symfony/Component/Messenger/Middleware/MiddlewareInterface.php b/src/Symfony/Component/Messenger/Middleware/MiddlewareInterface.php index 9826611f0c145..2fc2edaa39e92 100644 --- a/src/Symfony/Component/Messenger/Middleware/MiddlewareInterface.php +++ b/src/Symfony/Component/Messenger/Middleware/MiddlewareInterface.php @@ -12,11 +12,15 @@ namespace Symfony\Component\Messenger\Middleware; use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\ExceptionInterface; /** * @author Samuel Roze */ interface MiddlewareInterface { + /** + * @throws ExceptionInterface + */ public function handle(Envelope $envelope, StackInterface $stack): Envelope; } diff --git a/src/Symfony/Component/Messenger/Transport/Sender/SenderInterface.php b/src/Symfony/Component/Messenger/Transport/Sender/SenderInterface.php index 3414a40c3807a..a0ed6cf0d67b4 100644 --- a/src/Symfony/Component/Messenger/Transport/Sender/SenderInterface.php +++ b/src/Symfony/Component/Messenger/Transport/Sender/SenderInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Transport\Sender; use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\ExceptionInterface; /** * @author Samuel Roze @@ -25,6 +26,8 @@ interface SenderInterface * like delivery delay. * * If applicable, the returned Envelope should contain a TransportMessageIdStamp. + * + * @throws ExceptionInterface */ public function send(Envelope $envelope): Envelope; } From 4a665acc39127aa054fd23490a52e7261030a022 Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Mon, 4 Dec 2023 02:41:30 +0300 Subject: [PATCH 033/395] [PropertyAccess][Serializer] Fix "type unknown" on denormalize --- .../RequestPayloadValueResolverTest.php | 4 +-- .../Component/HttpKernel/composer.json | 4 +-- .../Exception/InvalidTypeException.php | 32 +++++++++++++++++++ .../PropertyAccess/PropertyAccessor.php | 6 ++-- .../Normalizer/AbstractObjectNormalizer.php | 3 +- .../Tests/Normalizer/ObjectNormalizerTest.php | 20 ++++++++++++ 6 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 src/Symfony/Component/PropertyAccess/Exception/InvalidTypeException.php diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 326551d87b57e..d5939dbfa83bd 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -253,7 +253,7 @@ public function testValidationNotPassed() $validationFailedException = $e->getPrevious(); $this->assertSame(422, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); - $this->assertSame('This value should be of type unknown.', $validationFailedException->getViolations()[0]->getMessage()); + $this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage()); $this->assertSame('Test', $validationFailedException->getViolations()[1]->getMessage()); } } @@ -665,7 +665,7 @@ public function testRequestPayloadValidationErrorCustomStatusCode() $validationFailedException = $e->getPrevious(); $this->assertSame(400, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); - $this->assertSame('This value should be of type unknown.', $validationFailedException->getViolations()[0]->getMessage()); + $this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage()); $this->assertSame('Test', $validationFailedException->getViolations()[1]->getMessage()); } } diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 09ac8fe4c162d..62d5f3eec7a56 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -35,9 +35,9 @@ "symfony/finder": "^6.4|^7.0", "symfony/http-client-contracts": "^2.5|^3", "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", + "symfony/property-access": "^7.1", "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/serializer": "^7.1", "symfony/stopwatch": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", diff --git a/src/Symfony/Component/PropertyAccess/Exception/InvalidTypeException.php b/src/Symfony/Component/PropertyAccess/Exception/InvalidTypeException.php new file mode 100644 index 0000000000000..f659ffd07e6da --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/InvalidTypeException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a type of given value does not match an expected type. + * + * @author Farhad Safarov + */ +class InvalidTypeException extends InvalidArgumentException +{ + public function __construct( + public readonly string $expectedType, + public readonly string $actualType, + public readonly string $propertyPath, + \Throwable $previous = null, + ) { + parent::__construct( + sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $expectedType, 'NULL' === $actualType ? 'null' : $actualType, $propertyPath), + previous: $previous, + ); + } +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index a763f3ae08344..8393a332459a0 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -18,7 +18,7 @@ use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\PropertyAccess\Exception\AccessException; -use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; +use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; @@ -192,12 +192,12 @@ private static function throwInvalidArgumentException(string $message, array $tr if (preg_match('/^\S+::\S+\(\): Argument #\d+ \(\$\S+\) must be of type (\S+), (\S+) given/', $message, $matches)) { [, $expectedType, $actualType] = $matches; - throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $expectedType, 'NULL' === $actualType ? 'null' : $actualType, $propertyPath), 0, $previous); + throw new InvalidTypeException($expectedType, $actualType, $propertyPath, $previous); } if (preg_match('/^Cannot assign (\S+) to property \S+::\$\S+ of type (\S+)$/', $message, $matches)) { [, $actualType, $expectedType] = $matches; - throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $expectedType, 'NULL' === $actualType ? 'null' : $actualType, $propertyPath), 0, $previous); + throw new InvalidTypeException($expectedType, $actualType, $propertyPath, $previous); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 487cd4bda4fd6..1361115e24687 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException as PropertyAccessInvalidArgumentException; +use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -374,7 +375,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $exception = NotNormalizableValueException::createForUnexpectedDataType( sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $data, - ['unknown'], + $e instanceof InvalidTypeException ? [$e->expectedType] : ['unknown'], $context['deserialization_path'] ?? null, false, $e->getCode(), diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index fa9f5c396067e..39ab6c4cf2ef5 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -14,11 +14,13 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -835,6 +837,24 @@ public function testNormalizeStdClass() $this->assertSame(['baz' => 'baz'], $this->normalizer->normalize($o2)); } + + public function testNotNormalizableValueInvalidType() + { + if (!class_exists(InvalidTypeException::class)) { + $this->markTestSkipped('Skipping test as the improvements on PropertyAccess are required.'); + } + + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('Expected argument of type "string", "array" given at property path "initialized"'); + + try { + $this->normalizer->denormalize(['initialized' => ['not a string']], TypedPropertiesObject::class, 'array'); + } catch (NotNormalizableValueException $e) { + $this->assertSame(['string'], $e->getExpectedTypes()); + + throw $e; + } + } } class ProxyObjectDummy extends ObjectDummy From 6ffd1736251d0941e35f174e755ba120f5b1205a Mon Sep 17 00:00:00 2001 From: Vic D'Elfant Date: Wed, 6 Dec 2023 17:13:01 +0100 Subject: [PATCH 034/395] [Mailer] Dispatch event for Postmark's "inactive recipient" API error --- .../Postmark/Event/PostmarkDeliveryEvent.php | 64 +++++++++++++++++++ .../Event/PostmarkDeliveryEventFactory.php | 34 ++++++++++ .../Bridge/Postmark/Event/PostmarkEvents.php | 17 +++++ .../PostmarkDeliveryEventFactoryTest.php | 26 ++++++++ .../Transport/PostmarkApiTransportTest.php | 34 ++++++++++ .../Transport/PostmarkApiTransport.php | 17 +++++ src/Symfony/Component/Mailer/CHANGELOG.md | 5 ++ 7 files changed, 197 insertions(+) create mode 100644 src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEvent.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEventFactory.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkEvents.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Event/PostmarkDeliveryEventFactoryTest.php diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEvent.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEvent.php new file mode 100644 index 0000000000000..e20335ad0f8b8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEvent.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Postmark\Event; + +use Symfony\Component\Mime\Header\Headers; + +class PostmarkDeliveryEvent +{ + public const CODE_INACTIVE_RECIPIENT = 406; + + private int $errorCode; + + private Headers $headers; + + private ?string $message; + + public function __construct(string $message, int $errorCode) + { + $this->message = $message; + $this->errorCode = $errorCode; + + $this->headers = new Headers(); + } + + public function getErrorCode(): int + { + return $this->errorCode; + } + + public function getHeaders(): Headers + { + return $this->headers; + } + + public function getMessage(): ?string + { + return $this->message; + } + + public function getMessageId(): ?string + { + if (!$this->headers->has('Message-ID')) { + return null; + } + + return $this->headers->get('Message-ID')->getBodyAsString(); + } + + public function setHeaders(Headers $headers): self + { + $this->headers = $headers; + + return $this; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEventFactory.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEventFactory.php new file mode 100644 index 0000000000000..8bdb1807bcfb7 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEventFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Postmark\Event; + +use Symfony\Component\Mime\Email; + +class PostmarkDeliveryEventFactory +{ + public function create(int $errorCode, string $message, Email $email): PostmarkDeliveryEvent + { + if (!$this->supports($errorCode)) { + throw new \InvalidArgumentException(sprintf('Error code "%s" is not supported.', $errorCode)); + } + + return (new PostmarkDeliveryEvent($message, $errorCode)) + ->setHeaders($email->getHeaders()); + } + + public function supports(int $errorCode): bool + { + return \in_array($errorCode, [ + PostmarkDeliveryEvent::CODE_INACTIVE_RECIPIENT, + ]); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkEvents.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkEvents.php new file mode 100644 index 0000000000000..68832e26873f9 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkEvents.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Postmark\Event; + +class PostmarkEvents +{ + public const DELIVERY = 'postmark.delivery'; +} diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Event/PostmarkDeliveryEventFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Event/PostmarkDeliveryEventFactoryTest.php new file mode 100644 index 0000000000000..fc1f11e2cdecb --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Event/PostmarkDeliveryEventFactoryTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Postmark\Tests\Event; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkDeliveryEvent; +use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkDeliveryEventFactory; + +class PostmarkDeliveryEventFactoryTest extends TestCase +{ + public function testFactorySupportsInactiveRecipient() + { + $factory = new PostmarkDeliveryEventFactory(); + + $this->assertTrue($factory->supports(PostmarkDeliveryEvent::CODE_INACTIVE_RECIPIENT)); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php index 0b8b18836fc5e..bc4ad754ae7fb 100644 --- a/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Mailer\Bridge\Postmark\Tests\Transport; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\JsonMockResponse; +use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkDeliveryEvent; use Symfony\Component\Mailer\Bridge\Postmark\Transport\MessageStreamHeader; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkApiTransport; use Symfony\Component\Mailer\Envelope; @@ -119,6 +121,38 @@ public function testSendThrowsForErrorResponse() $transport->send($mail); } + public function testSendDeliveryEventIsDispatched() + { + $client = new MockHttpClient(static fn (string $method, string $url, array $options): ResponseInterface => new JsonMockResponse(['Message' => 'Inactive recipient', 'ErrorCode' => 406], [ + 'http_code' => 422, + ])); + + $mail = new Email(); + $mail->subject('Hello!') + ->to(new Address('saif.gmati@symfony.com', 'Saif Eddin')) + ->from(new Address('fabpot@symfony.com', 'Fabien')) + ->text('Hello There!'); + + $expectedEvent = (new PostmarkDeliveryEvent('Inactive recipient', 406)) + ->setHeaders($mail->getHeaders()); + + $dispatcher = $this->createMock(EventDispatcherInterface::class); + $dispatcher + ->method('dispatch') + ->willReturnCallback(function ($event) use ($expectedEvent) { + if ($event instanceof PostmarkDeliveryEvent) { + $this->assertEquals($event, $expectedEvent); + } + + return $event; + }); + + $transport = new PostmarkApiTransport('KEY', $client, $dispatcher); + $transport->setPort(8984); + + $transport->send($mail); + } + public function testTagAndMetadataAndMessageStreamHeaders() { $email = new Email(); diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php index 22ed262924f6d..94cd6bdcd029b 100644 --- a/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php @@ -13,6 +13,8 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkDeliveryEventFactory; +use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkEvents; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\HttpTransportException; use Symfony\Component\Mailer\Exception\TransportException; @@ -33,6 +35,8 @@ class PostmarkApiTransport extends AbstractApiTransport { private const HOST = 'api.postmarkapp.com'; + private ?EventDispatcherInterface $dispatcher; + private string $key; private ?string $messageStream = null; @@ -40,6 +44,7 @@ class PostmarkApiTransport extends AbstractApiTransport public function __construct(string $key, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { $this->key = $key; + $this->dispatcher = $dispatcher; parent::__construct($client, $dispatcher, $logger); } @@ -69,6 +74,18 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e } if (200 !== $statusCode) { + $eventFactory = new PostmarkDeliveryEventFactory(); + + // Some delivery issues can be handled silently - route those through EventDispatcher + if (null !== $this->dispatcher && $eventFactory->supports($result['ErrorCode'])) { + $this->dispatcher->dispatch( + $eventFactory->create($result['ErrorCode'], $result['Message'], $email), + PostmarkEvents::DELIVERY, + ); + + return $response; + } + throw new HttpTransportException('Unable to send an email: '.$result['Message'].sprintf(' (code %d).', $result['ErrorCode']), $response); } diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index 0645df470b227..d58cf3d832353 100644 --- a/src/Symfony/Component/Mailer/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Dispatch Postmark's "406 - Inactive recipient" API error code as a `PostmarkDeliveryEvent` instead of throwing an exception + 7.0 --- From ecc628aa712d319a97876303d52fd564802e21f5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 8 Dec 2023 15:23:08 +0100 Subject: [PATCH 035/395] Fx README files --- src/Symfony/Component/Clock/README.md | 4 ++-- src/Symfony/Component/Dotenv/README.md | 4 ++-- src/Symfony/Component/ErrorHandler/README.md | 4 ++-- src/Symfony/Component/Mailer/README.md | 4 ++-- src/Symfony/Component/PasswordHasher/README.md | 4 ++-- src/Symfony/Component/RateLimiter/README.md | 4 ++-- src/Symfony/Component/Routing/README.md | 4 ++-- src/Symfony/Component/Security/Core/README.md | 4 ++-- src/Symfony/Component/Security/Http/README.md | 4 ++-- src/Symfony/Component/Stopwatch/README.md | 4 ++-- src/Symfony/Component/WebLink/README.md | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Symfony/Component/Clock/README.md b/src/Symfony/Component/Clock/README.md index e860a64740d75..e80b5d3779fb7 100644 --- a/src/Symfony/Component/Clock/README.md +++ b/src/Symfony/Component/Clock/README.md @@ -6,8 +6,8 @@ Symfony Clock decouples applications from the system clock. Getting Started --------------- -``` -$ composer require symfony/clock +```bash +composer require symfony/clock ``` ```php diff --git a/src/Symfony/Component/Dotenv/README.md b/src/Symfony/Component/Dotenv/README.md index 08c90fcc88022..2a1cc02ccfcb8 100644 --- a/src/Symfony/Component/Dotenv/README.md +++ b/src/Symfony/Component/Dotenv/README.md @@ -7,8 +7,8 @@ accessible via `$_SERVER` or `$_ENV`. Getting Started --------------- -``` -$ composer require symfony/dotenv +```bash +composer require symfony/dotenv ``` ```php diff --git a/src/Symfony/Component/ErrorHandler/README.md b/src/Symfony/Component/ErrorHandler/README.md index 12c0bfa6d6ca9..68904dd7073c4 100644 --- a/src/Symfony/Component/ErrorHandler/README.md +++ b/src/Symfony/Component/ErrorHandler/README.md @@ -6,8 +6,8 @@ The ErrorHandler component provides tools to manage errors and ease debugging PH Getting Started --------------- -``` -$ composer require symfony/error-handler +```bash +composer require symfony/error-handler ``` ```php diff --git a/src/Symfony/Component/Mailer/README.md b/src/Symfony/Component/Mailer/README.md index 43e7dfe054592..04d8f76694a2b 100644 --- a/src/Symfony/Component/Mailer/README.md +++ b/src/Symfony/Component/Mailer/README.md @@ -6,8 +6,8 @@ The Mailer component helps sending emails. Getting Started --------------- -``` -$ composer require symfony/mailer +```bash +composer require symfony/mailer ``` ```php diff --git a/src/Symfony/Component/PasswordHasher/README.md b/src/Symfony/Component/PasswordHasher/README.md index 0878746fca38c..ca93044ef5872 100644 --- a/src/Symfony/Component/PasswordHasher/README.md +++ b/src/Symfony/Component/PasswordHasher/README.md @@ -6,8 +6,8 @@ The PasswordHasher component provides secure password hashing utilities. Getting Started --------------- -``` -$ composer require symfony/password-hasher +```bash +composer require symfony/password-hasher ``` ```php diff --git a/src/Symfony/Component/RateLimiter/README.md b/src/Symfony/Component/RateLimiter/README.md index d319b722e9640..e4830752a2350 100644 --- a/src/Symfony/Component/RateLimiter/README.md +++ b/src/Symfony/Component/RateLimiter/README.md @@ -7,8 +7,8 @@ rate limit input and output in your application. Getting Started --------------- -``` -$ composer require symfony/rate-limiter +```bash +composer require symfony/rate-limiter ``` ```php diff --git a/src/Symfony/Component/Routing/README.md b/src/Symfony/Component/Routing/README.md index b5a603d6e5084..75580363f18d6 100644 --- a/src/Symfony/Component/Routing/README.md +++ b/src/Symfony/Component/Routing/README.md @@ -6,8 +6,8 @@ The Routing component maps an HTTP request to a set of configuration variables. Getting Started --------------- -``` -$ composer require symfony/routing +```bash +composer require symfony/routing ``` ```php diff --git a/src/Symfony/Component/Security/Core/README.md b/src/Symfony/Component/Security/Core/README.md index 5bb87c3c753ad..b70682902f95e 100644 --- a/src/Symfony/Component/Security/Core/README.md +++ b/src/Symfony/Component/Security/Core/README.md @@ -8,8 +8,8 @@ so called user providers that hold the users credentials. Getting Started --------------- -``` -$ composer require symfony/security-core +```bash +composer require symfony/security-core ``` ```php diff --git a/src/Symfony/Component/Security/Http/README.md b/src/Symfony/Component/Security/Http/README.md index 4ea2ee1235cf4..4edb0b1450fed 100644 --- a/src/Symfony/Component/Security/Http/README.md +++ b/src/Symfony/Component/Security/Http/README.md @@ -8,8 +8,8 @@ provides authenticators to authenticate visitors. Getting Started --------------- -``` -$ composer require symfony/security-http +```bash +composer require symfony/security-http ``` Sponsor diff --git a/src/Symfony/Component/Stopwatch/README.md b/src/Symfony/Component/Stopwatch/README.md index 13a9dfa5f4f1f..824ddfd69be05 100644 --- a/src/Symfony/Component/Stopwatch/README.md +++ b/src/Symfony/Component/Stopwatch/README.md @@ -6,8 +6,8 @@ The Stopwatch component provides a way to profile code. Getting Started --------------- -``` -$ composer require symfony/stopwatch +```bash +composer require symfony/stopwatch ``` ```php diff --git a/src/Symfony/Component/WebLink/README.md b/src/Symfony/Component/WebLink/README.md index fe33a9c497f8e..7e958a0f65e60 100644 --- a/src/Symfony/Component/WebLink/README.md +++ b/src/Symfony/Component/WebLink/README.md @@ -15,8 +15,8 @@ wiki](http://microformats.org/wiki/existing-rel-values#HTML5_link_type_extension Getting Started --------------- -``` -$ composer require symfony/web-link +```bash +composer require symfony/web-link ``` ```php From 266e50f4c489d93c1b0d2bafbd9fb411643b31f4 Mon Sep 17 00:00:00 2001 From: Thomas Durand Date: Fri, 8 Dec 2023 09:28:11 +0100 Subject: [PATCH 036/395] [HttpClient] Add HttpOptions->addHeader as a shortcut to add an header in an existing options object --- src/Symfony/Component/HttpClient/HttpOptions.php | 11 +++++++++++ .../Component/HttpClient/Tests/HttpOptionsTest.php | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Symfony/Component/HttpClient/HttpOptions.php b/src/Symfony/Component/HttpClient/HttpOptions.php index 57590d3c131fc..8eba8ba055f7c 100644 --- a/src/Symfony/Component/HttpClient/HttpOptions.php +++ b/src/Symfony/Component/HttpClient/HttpOptions.php @@ -63,6 +63,17 @@ public function setQuery(array $query): static return $this; } + /** + * @return $this + */ + public function addHeader(string $key, string $value): static + { + $this->options['headers'] ??= []; + $this->options['headers'][$key] = $value; + + return $this; + } + /** * @return $this */ diff --git a/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php b/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php index 9dbbff7dd9364..487a889d454f7 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php @@ -39,4 +39,15 @@ public function testSetAuthBearer() { $this->assertSame('foobar', (new HttpOptions())->setAuthBearer('foobar')->toArray()['auth_bearer']); } + + public function testAddHeader() + { + $options = new HttpOptions(); + $options->addHeader('Accept', 'application/json'); + $this->assertSame(['Accept' => 'application/json'], $options->toArray()['headers']); + $options->addHeader('Accept-Language', 'en-US,en;q=0.5'); + $this->assertSame(['Accept' => 'application/json', 'Accept-Language' => 'en-US,en;q=0.5'], $options->toArray()['headers']); + $options->addHeader('Accept', 'application/html'); + $this->assertSame(['Accept' => 'application/html', 'Accept-Language' => 'en-US,en;q=0.5'], $options->toArray()['headers']); + } } From dec3110c5a9ccc6db3819ef6cd7d4849a1eef4a4 Mon Sep 17 00:00:00 2001 From: Rafael Villa Verde Date: Fri, 1 Dec 2023 01:03:58 -0300 Subject: [PATCH 037/395] [Mailer] Add Azure bridge --- .../FrameworkExtension.php | 1 + .../Mailer/Bridge/Azure/.gitattributes | 4 + .../Component/Mailer/Bridge/Azure/.gitignore | 2 + .../Mailer/Bridge/Azure/CHANGELOG.md | 7 + .../Component/Mailer/Bridge/Azure/LICENSE | 19 ++ .../Component/Mailer/Bridge/Azure/README.md | 28 ++ .../Tests/Transport/AzureApiTransportTest.php | 128 ++++++++ .../Transport/AzureTransportFactoryTest.php | 79 +++++ .../Azure/Transport/AzureApiTransport.php | 282 ++++++++++++++++++ .../Azure/Transport/AzureTransportFactory.php | 42 +++ .../Mailer/Bridge/Azure/composer.json | 32 ++ .../Mailer/Bridge/Azure/phpunit.xml.dist | 31 ++ src/Symfony/Component/Mailer/Transport.php | 2 + 13 files changed, 657 insertions(+) create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/.gitattributes create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/.gitignore create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/CHANGELOG.md create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/LICENSE create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/README.md create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/Tests/Transport/AzureApiTransportTest.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/Tests/Transport/AzureTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/Transport/AzureApiTransport.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/Transport/AzureTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/composer.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Azure/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 95c8d8fa11019..8272d24c7737c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2559,6 +2559,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co } $classToServices = [ + MailerBridge\Azure\Transport\AzureTransportFactory::class => 'mailer.transport_factory.azure', MailerBridge\Brevo\Transport\BrevoTransportFactory::class => 'mailer.transport_factory.brevo', MailerBridge\Google\Transport\GmailTransportFactory::class => 'mailer.transport_factory.gmail', MailerBridge\Infobip\Transport\InfobipTransportFactory::class => 'mailer.transport_factory.infobip', diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/.gitattributes b/src/Symfony/Component/Mailer/Bridge/Azure/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/.gitignore b/src/Symfony/Component/Mailer/Bridge/Azure/.gitignore new file mode 100644 index 0000000000000..d1502b087b4d4 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/.gitignore @@ -0,0 +1,2 @@ +vendor/ +composer.lock diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Azure/CHANGELOG.md new file mode 100644 index 0000000000000..5be39cbeeb951 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.1 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/LICENSE b/src/Symfony/Component/Mailer/Bridge/Azure/LICENSE new file mode 100644 index 0000000000000..3ed9f412ce53d --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/README.md b/src/Symfony/Component/Mailer/Bridge/Azure/README.md new file mode 100644 index 0000000000000..acd9cc25abb53 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/README.md @@ -0,0 +1,28 @@ +Microsoft Azure Mailer +====================== + +Provides [Azure Communication Services Email](https://learn.microsoft.com/en-us/azure/communication-services/concepts/email/email-overview) integration for Symfony Mailer. + +Configuration example: + +```env +# API +MAILER_DSN=azure+api://ACS_RESOURCE_NAME:KEY@default + +#API with options + +MAILER_DSN=azure+api://ACS_RESOURCE_NAME:KEY@default?api_version=2023-03-31&disable_tracking=false +``` + +where: + - `ACS_RESOURCE_NAME` is your Azure Communication Services endpoint resource name (https://ACS_RESOURCE_NAME.communication.azure.com) + - `KEY` is your Azure Communication Services Email API Key + +Resources +--------- + + * [Microsoft Azure (ACS) Email API Docs](https://learn.microsoft.com/en-us/rest/api/communication/dataplane/email/send) + * [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) \ No newline at end of file diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/Tests/Transport/AzureApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Azure/Tests/Transport/AzureApiTransportTest.php new file mode 100644 index 0000000000000..196cdb7b6b1b7 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/Tests/Transport/AzureApiTransportTest.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Azure\Tests\Transport; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\JsonMockResponse; +use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureApiTransport; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Header\MetadataHeader; +use Symfony\Component\Mailer\Header\TagHeader; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class AzureApiTransportTest extends TestCase +{ + /** + * @dataProvider getTransportData + */ + public function testToString(AzureApiTransport $transport, string $expected) + { + $this->assertSame($expected, (string) $transport); + } + + public static function getTransportData(): array + { + return [ + [ + new AzureApiTransport('KEY', 'ACS_RESOURCE_NAME'), + 'azure+api://ACS_RESOURCE_NAME.communication.azure.com', + ], + ]; + } + + public function testCustomHeader() + { + $email = new Email(); + $email->getHeaders()->addTextHeader('foo', 'bar'); + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new AzureApiTransport('KEY', 'ACS_RESOURCE_NAME'); + $method = new \ReflectionMethod(AzureApiTransport::class, 'getPayload'); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('headers', $payload); + $this->assertArrayHasKey('foo', $payload['headers']); + $this->assertEquals('bar', $payload['headers']['foo']); + } + + public function testSend() + { + $client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://my-acs-resource.communication.azure.com/emails:send?api-version=2023-03-31', $url); + + $body = json_decode($options['body'], true); + + $message = $body['content']; + $this->assertSame('normal', $body['importance']); + // $this->assertSame('Fabien', $message['from_name']); + $this->assertSame('fabpot@symfony.com', $body['senderAddress']); + $this->assertSame('Saif Eddin', $body['recipients']['to'][0]['displayName']); + $this->assertSame('saif.gmati@symfony.com', $body['recipients']['to'][0]['address']); + $this->assertSame('Hello!', $message['subject']); + $this->assertSame('Hello There!', $message['plainText']); + + return new JsonMockResponse([ + 'id' => 'foobar', + ], [ + 'http_code' => 202, + ]); + }); + + $transport = new AzureApiTransport('KEY', 'my-acs-resource', true, '2023-03-31', $client); + + $mail = new Email(); + $mail->subject('Hello!') + ->to(new Address('saif.gmati@symfony.com', 'Saif Eddin')) + ->from(new Address('fabpot@symfony.com', 'Fabien')) + ->text('Hello There!'); + + $message = $transport->send($mail); + + $this->assertSame('foobar', $message->getMessageId()); + } + + public function testTagAndMetadataHeaders() + { + $email = new Email(); + $email->getHeaders()->add(new TagHeader('category-one')); + $email->getHeaders()->add(new MetadataHeader('Color', 'blue')); + $email->getHeaders()->add(new MetadataHeader('Client-ID', '12345')); + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new AzureApiTransport('KEY', 'ACS_RESOURCE_NAME'); + $method = new \ReflectionMethod(AzureApiTransport::class, 'getPayload'); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('headers', $payload); + $this->assertArrayHasKey('X-Tag', $payload['headers']); + $this->assertArrayHasKey('X-Metadata-Color', $payload['headers']); + $this->assertArrayHasKey('X-Metadata-Client-ID', $payload['headers']); + + $this->assertCount(3, $payload['headers']); + + $this->assertSame('category-one', $payload['headers']['X-Tag']); + $this->assertSame('blue', $payload['headers']['X-Metadata-Color']); + $this->assertSame('12345', $payload['headers']['X-Metadata-Client-ID']); + } + + public function testItDoesNotAllowToAddResourceNameWithDot() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Resource name cannot contain or end with a dot'); + + new AzureApiTransport('KEY', 'ACS_RESOURCE_NAME.'); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/Tests/Transport/AzureTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Azure/Tests/Transport/AzureTransportFactoryTest.php new file mode 100644 index 0000000000000..4250ed6adfac6 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/Tests/Transport/AzureTransportFactoryTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Azure\Tests\Transport; + +use Psr\Log\NullLogger; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureApiTransport; +use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureTransportFactory; +use Symfony\Component\Mailer\Test\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class AzureTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new AzureTransportFactory(null, new MockHttpClient(), new NullLogger()); + } + + public static function supportsProvider(): iterable + { + yield [ + new Dsn('azure', 'default'), + true, + ]; + + yield [ + new Dsn('azure+api', 'default'), + true, + ]; + } + + public static function createProvider(): iterable + { + yield [ + new Dsn('azure', 'default', self::USER, self::PASSWORD), + new AzureApiTransport(self::PASSWORD, self::USER, false, '2023-03-31', new MockHttpClient(), null, new NullLogger()), + ]; + yield [ + new Dsn('azure', 'ACS_RESOURCE_NAME', self::USER, self::PASSWORD), + (new AzureApiTransport(self::PASSWORD, self::USER, false, '2023-03-31', new MockHttpClient(), null, new NullLogger()))->setHost('ACS_RESOURCE_NAME'), + ]; + yield [ + new Dsn('azure+api', 'default', self::USER, self::PASSWORD), + new AzureApiTransport(self::PASSWORD, self::USER, false, '2023-03-31', new MockHttpClient(), null, new NullLogger()), + ]; + yield [ + new Dsn('azure+api', 'ACS_RESOURCE_NAME', self::USER, self::PASSWORD), + (new AzureApiTransport(self::PASSWORD, self::USER, false, '2023-03-31', new MockHttpClient(), null, new NullLogger()))->setHost('ACS_RESOURCE_NAME'), + ]; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield [ + new Dsn('azure+foo', 'default', self::USER, self::PASSWORD), + 'The "azure+foo" scheme is not supported; supported schemes for mailer "azure" are: "azure", "azure+api".', + ]; + } + + public static function incompleteDsnProvider(): iterable + { + yield [new Dsn('azure', 'default')]; + yield [new Dsn('azure', 'default', self::USER)]; + yield [new Dsn('azure', 'default', null, self::PASSWORD)]; + yield [new Dsn('azure+api', 'default')]; + yield [new Dsn('azure+api', 'default', self::USER)]; + yield [new Dsn('azure+api', 'default', null, self::PASSWORD)]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/Transport/AzureApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Azure/Transport/AzureApiTransport.php new file mode 100644 index 0000000000000..375976971155f --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/Transport/AzureApiTransport.php @@ -0,0 +1,282 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Azure\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\AbstractApiTransport; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class AzureApiTransport extends AbstractApiTransport +{ + private const HOST = '%s.communication.azure.com'; + + /** + * User Access Key from Azure Communication Service (Primary or Secondary key). + */ + private string $key; + + /** + * The endpoint API URL to which to POST emails to Azure + * https://{acsResourceName}.communication.azure.com/. + */ + private string $resourceName; + + /** + * The version of API to invoke. + */ + private string $apiVersion; + + /** + * Indicates whether user engagement tracking should be disabled. + */ + private bool $disableTracking; + + public function __construct(string $key, string $resourceName, bool $disableTracking = false, string $apiVersion = '2023-03-31', HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + if (str_contains($resourceName, '.') || str_ends_with($resourceName, '.')) { + throw new \Exception('Resource name cannot contain or end with a dot.'); + } + + $this->resourceName = $resourceName; + $this->key = $key; + $this->apiVersion = $apiVersion; + $this->disableTracking = $disableTracking; + parent::__construct($client, $dispatcher, $logger); + } + + public function __toString(): string + { + return sprintf('azure+api://%s', $this->getAzureCSEndpoint()); + } + + /** + * Queues an email message to be sent to one or more recipients. + */ + protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface + { + $endpoint = $this->getAzureCSEndpoint().'/emails:send?api-version='.$this->apiVersion; + $payload = $this->getPayload($email, $envelope); + + $response = $this->client->request('POST', 'https://'.$endpoint, [ + 'body' => json_encode($payload), + 'headers' => $this->getSignedHeaders($payload, $email), + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new HttpTransportException('Could not reach the remote Azure server.', $response, 0, $e); + } + + if (202 !== $statusCode) { + try { + $result = $response->toArray(false); + throw new HttpTransportException('Unable to send an email (.'.$result['error']['code'].'): '.$result['error']['message'], $response, $statusCode); + } catch (DecodingExceptionInterface $e) { + throw new HttpTransportException('Unable to send an email: '.$response->getContent(false).sprintf(' (code %d).', $statusCode), $response, 0, $e); + } + } + + $sentMessage->setMessageId(json_decode($response->getContent(false), true)['id']); + + return $response; + } + + /** + * Get the message request body. + */ + private function getPayload(Email $email, Envelope $envelope): array + { + $addressStringifier = function (Address $address) { + $stringified = ['address' => $address->getAddress()]; + + if ($address->getName()) { + $stringified['displayName'] = $address->getName(); + } + + return $stringified; + }; + + $data = [ + 'content' => [ + 'html' => $email->getHtmlBody(), + 'plainText' => $email->getTextBody(), + 'subject' => $email->getSubject(), + ], + 'recipients' => [ + 'to' => array_map($addressStringifier, $this->getRecipients($email, $envelope)), + ], + 'senderAddress' => $envelope->getSender()->getAddress(), + 'attachments' => $this->getMessageAttachments($email), + 'userEngagementTrackingDisabled' => $this->disableTracking, + 'headers' => empty($headers = $this->getMessageCustomHeaders($email)) ? null : $headers, + 'importance' => $this->getPriorityLevel($email->getPriority()), + ]; + + if ($emails = array_map($addressStringifier, $email->getCc())) { + $data['recipients']['cc'] = $emails; + } + + if ($emails = array_map($addressStringifier, $email->getBcc())) { + $data['recipients']['bcc'] = $emails; + } + + if ($emails = array_map($addressStringifier, $email->getReplyTo())) { + $data['replyTo'] = $emails; + } + + return $data; + } + + /** + * List of attachments. Please note that the service limits the total size + * of an email request (which includes attachments) to 10 MB. + */ + private function getMessageAttachments(Email $email): array + { + $attachments = []; + foreach ($email->getAttachments() as $attachment) { + $headers = $attachment->getPreparedHeaders(); + $filename = $headers->getHeaderParameter('Content-Disposition', 'filename'); + $disposition = $headers->getHeaderBody('Content-Disposition'); + + $att = [ + 'name' => $filename, + 'contentInBase64' => base64_encode(str_replace("\r\n", '', $attachment->bodyToString())), + 'contentType' => $headers->get('Content-Type')->getBody(), + ]; + + if ('inline' === $disposition) { + $att['content_id'] = $filename; + } + + $attachments[] = $att; + } + + return $attachments; + } + + /** + * The communication domain host, for example my-acs-resource-name.communication.azure.com. + */ + private function getAzureCSEndpoint(): string + { + return !empty($this->host) ? $this->host : sprintf(self::HOST, $this->resourceName); + } + + private function generateContentHash(string $content): string + { + return base64_encode(hash('sha256', $content, true)); + } + + /** + * Generate sha256 hash and encode to base64 to produces the digest string. + */ + private function generateAuthenticationSignature(string $content): string + { + $key = base64_decode($this->key); + $hashedBytes = hash_hmac('sha256', mb_convert_encoding($content, 'UTF-8'), $key, true); + + return base64_encode($hashedBytes); + } + + /** + * Get authenticated headers for signed request,. + */ + private function getSignedHeaders(array $payload, Email $message): array + { + // HTTP Method verb (uppercase) + $verb = 'POST'; + + // Request time + $datetime = new \DateTime('now', new \DateTimeZone('UTC')); + $utcNow = $datetime->format('D, d M Y H:i:s \G\M\T'); + + // Content hash signature + $contentHash = $this->generateContentHash(json_encode($payload)); + + // ACS Endpoint + $host = str_replace('https://', '', $this->getAzureCSEndpoint()); + + // Sendmail endpoint from communication email delivery service + $urlPathAndQuery = '/emails:send?api-version='.$this->apiVersion; + + // Signed request headers + $stringToSign = "{$verb}\n{$urlPathAndQuery}\n{$utcNow};{$host};{$contentHash}"; + + // Authenticate headers with ACS primary or secondary key + $signature = $this->generateAuthenticationSignature($stringToSign); + + // get GUID part of message id to identify the long running operation + $messageId = $this->generateMessageId(); + + return [ + 'Content-Type' => 'application/json', + 'repeatability-request-id' => $messageId, + 'Operation-Id' => $messageId, + 'repeatability-first-sent' => $utcNow, + 'x-ms-date' => $utcNow, + 'x-ms-content-sha256' => $contentHash, + 'x-ms-client-request-id' => $messageId, + 'Authorization' => "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature={$signature}", + ]; + } + + /** + * Can be used to identify the long running operation. + */ + private function generateMessageId(): string + { + $data = random_bytes(16); + \assert(16 == \strlen($data)); + $data[6] = \chr(\ord($data[6]) & 0x0F | 0x40); + $data[8] = \chr(\ord($data[8]) & 0x3F | 0x80); + + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); + } + + private function getMessageCustomHeaders(Email $email): array + { + $headers = []; + + $headersToBypass = ['x-ms-client-request-id', 'operation-id', 'authorization', 'x-ms-content-sha256', 'received', 'dkim-signature', 'content-transfer-encoding', 'from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'reply-to']; + + foreach ($email->getHeaders()->all() as $name => $header) { + if (\in_array($name, $headersToBypass, true)) { + continue; + } + $headers[$header->getName()] = $header->getBodyAsString(); + } + + return $headers; + } + + private function getPriorityLevel(string $priority): ?string + { + return match ((int) $priority) { + Email::PRIORITY_HIGHEST => 'highest', + Email::PRIORITY_HIGH => 'high', + Email::PRIORITY_NORMAL => 'normal', + Email::PRIORITY_LOW => 'low', + Email::PRIORITY_LOWEST => 'lowest', + }; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/Transport/AzureTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Azure/Transport/AzureTransportFactory.php new file mode 100644 index 0000000000000..71128c120a652 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/Transport/AzureTransportFactory.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Azure\Transport; + +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +final class AzureTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + + if (!\in_array($scheme, ['azure+api', 'azure'], true)) { + throw new UnsupportedSchemeException($dsn, 'azure', $this->getSupportedSchemes()); + } + + $user = $this->getUser($dsn); // resourceName + $password = $this->getPassword($dsn); // apiKey + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $apiVersion = $dsn->getOption('api_version', '2023-03-31'); + $disableTracking = (bool) $dsn->getOption('disable_tracking', false); + + return (new AzureApiTransport($password, $user, $disableTracking, $apiVersion, $this->client, $this->dispatcher, $this->logger))->setHost($host); + } + + protected function getSupportedSchemes(): array + { + return ['azure', 'azure+api']; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/composer.json b/src/Symfony/Component/Mailer/Bridge/Azure/composer.json new file mode 100644 index 0000000000000..a031a1f9be9f2 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/composer.json @@ -0,0 +1,32 @@ +{ + "name": "symfony/azure-mailer", + "type": "symfony-mailer-bridge", + "description": "Symfony Microsoft Azure Mailer Bridge", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Rafael Villa Verde", + "homepage": "https://github.com/hafael" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/mailer": "^6.2.7|^7.0" + }, + "require-dev": { + "symfony/http-client": "^6.0|^7.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Azure\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/phpunit.xml.dist b/src/Symfony/Component/Mailer/Bridge/Azure/phpunit.xml.dist new file mode 100644 index 0000000000000..806393ddcd0bd --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Azure/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Resources + ./Tests + ./vendor + + + diff --git a/src/Symfony/Component/Mailer/Transport.php b/src/Symfony/Component/Mailer/Transport.php index 2bbaff28a8676..fae3adf3ca862 100644 --- a/src/Symfony/Component/Mailer/Transport.php +++ b/src/Symfony/Component/Mailer/Transport.php @@ -14,6 +14,7 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureTransportFactory; use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory; @@ -45,6 +46,7 @@ final class Transport { private const FACTORY_CLASSES = [ + AzureTransportFactory::class, BrevoTransportFactory::class, GmailTransportFactory::class, InfobipTransportFactory::class, From c715b5594b9e0985b056e62d582c6695e42e9418 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 9 Dec 2023 12:49:38 +0100 Subject: [PATCH 038/395] [Mailer][Azure] Fix deps --- src/Symfony/Component/Mailer/Bridge/Azure/composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/composer.json b/src/Symfony/Component/Mailer/Bridge/Azure/composer.json index a031a1f9be9f2..1adb82dfd5cbe 100644 --- a/src/Symfony/Component/Mailer/Bridge/Azure/composer.json +++ b/src/Symfony/Component/Mailer/Bridge/Azure/composer.json @@ -16,11 +16,11 @@ } ], "require": { - "php": ">=8.1", - "symfony/mailer": "^6.2.7|^7.0" + "php": ">=8.2", + "symfony/mailer": "^6.4|^7.0" }, "require-dev": { - "symfony/http-client": "^6.0|^7.0" + "symfony/http-client": "^6.4|^7.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Azure\\": "" }, From 39587cbf00ae4eae00a5c3a7dda1513e88c43ec2 Mon Sep 17 00:00:00 2001 From: Cristoforo Cervino Date: Tue, 13 Apr 2021 20:02:24 +0200 Subject: [PATCH 039/395] [Form] Errors Property Paths mismatch CollectionType children when removing an entry --- src/Symfony/Component/Form/CHANGELOG.md | 1 + .../Core/EventListener/ResizeFormListener.php | 18 +- .../Extension/Core/Type/CollectionType.php | 5 +- .../Type/FormTypeValidatorExtensionTest.php | 184 ++++++++++++++++++ .../Form/Tests/Fixtures/Organization.php | 32 +++ 5 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/Organization.php diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 273a71c0cde51..2035e8f805a53 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Deprecate not configuring the `default_protocol` option of the `UrlType`, it will default to `null` in 8.0 + * Add a `keep_as_list` option to `CollectionType` 7.0 --- diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php index 482007d53b943..641f16525770e 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php @@ -31,8 +31,9 @@ class ResizeFormListener implements EventSubscriberInterface protected bool $allowDelete; private \Closure|bool $deleteEmpty; + private bool $keepAsList; - public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, bool|callable $deleteEmpty = false, array $prototypeOptions = null) + public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, bool|callable $deleteEmpty = false, array $prototypeOptions = null, bool $keepAsList = false) { $this->type = $type; $this->allowAdd = $allowAdd; @@ -40,6 +41,7 @@ public function __construct(string $type, array $options = [], bool $allowAdd = $this->options = $options; $this->deleteEmpty = \is_bool($deleteEmpty) ? $deleteEmpty : $deleteEmpty(...); $this->prototypeOptions = $prototypeOptions ?? $options; + $this->keepAsList = $keepAsList; } public static function getSubscribedEvents(): array @@ -153,6 +155,20 @@ public function onSubmit(FormEvent $event): void } } + if ($this->keepAsList) { + $formReindex = []; + foreach ($form as $name => $child) { + $formReindex[] = $child; + $form->remove($name); + } + foreach ($formReindex as $index => $child) { + $form->add($index, $this->type, array_replace([ + 'property_path' => '['.$index.']', + ], $this->options)); + } + $data = array_values($data); + } + $event->setData($data); } } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php index c9d3ec5b7c6e2..3cef931526e0d 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php @@ -45,7 +45,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $options['allow_add'], $options['allow_delete'], $options['delete_empty'], - $resizePrototypeOptions + $resizePrototypeOptions, + $options['keep_as_list'] ); $builder->addEventSubscriber($resizeListener); @@ -114,12 +115,14 @@ public function configureOptions(OptionsResolver $resolver): void 'prototype_options' => [], 'delete_empty' => false, 'invalid_message' => 'The collection is invalid.', + 'keep_as_list' => false, ]); $resolver->setNormalizer('entry_options', $entryOptionsNormalizer); $resolver->setAllowedTypes('delete_empty', ['bool', 'callable']); $resolver->setAllowedTypes('prototype_options', 'array'); + $resolver->setAllowedTypes('keep_as_list', ['bool']); } public function getBlockPrefix(): string diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php index 3b4cd77396c60..a1d1a38402892 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php @@ -15,9 +15,12 @@ use Symfony\Component\Form\Form; use Symfony\Component\Form\Forms; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; +use Symfony\Component\Form\Tests\Extension\Core\Type\CollectionTypeTest; use Symfony\Component\Form\Tests\Extension\Core\Type\FormTypeTest; use Symfony\Component\Form\Tests\Extension\Core\Type\TextTypeTest; use Symfony\Component\Form\Tests\Fixtures\Author; +use Symfony\Component\Form\Tests\Fixtures\AuthorType; +use Symfony\Component\Form\Tests\Fixtures\Organization; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Length; @@ -158,4 +161,185 @@ protected function createForm(array $options = []) { return $this->factory->create(FormTypeTest::TESTED_TYPE, null, $options); } + + public function testCollectionTypeKeepAsListOptionFalse() + { + $formMetadata = new ClassMetadata(Form::class); + $authorMetadata = (new ClassMetadata(Author::class)) + ->addPropertyConstraint('firstName', new NotBlank()); + $organizationMetadata = (new ClassMetadata(Organization::class)) + ->addPropertyConstraint('authors', new Valid()); + $metadataFactory = $this->createMock(MetadataFactoryInterface::class); + $metadataFactory->expects($this->any()) + ->method('getMetadataFor') + ->willReturnCallback(static function ($classOrObject) use ($formMetadata, $authorMetadata, $organizationMetadata) { + if (Author::class === $classOrObject || $classOrObject instanceof Author) { + return $authorMetadata; + } + + if (Organization::class === $classOrObject || $classOrObject instanceof Organization) { + return $organizationMetadata; + } + + if (Form::class === $classOrObject || $classOrObject instanceof Form) { + return $formMetadata; + } + + return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : $classOrObject::class); + }); + + $validator = Validation::createValidatorBuilder() + ->setMetadataFactory($metadataFactory) + ->getValidator(); + + $form = Forms::createFormFactoryBuilder() + ->addExtension(new ValidatorExtension($validator)) + ->getFormFactory() + ->create(FormTypeTest::TESTED_TYPE, new Organization([]), [ + 'data_class' => Organization::class, + 'by_reference' => false, + ]) + ->add('authors', CollectionTypeTest::TESTED_TYPE, [ + 'entry_type' => AuthorType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'keep_as_list' => false, + ]) + ; + + $form->submit([ + 'authors' => [ + 0 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName1', + ], + // key "1" could be missing if we add 4 blank form entries and then remove it. + 2 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName3', + ], + 3 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName3', + ], + ], + ]); + + // Form does have 3 not blank errors + $errors = $form->getErrors(true); + $this->assertCount(3, $errors); + + // Form behaves as expected. It has index 0, 2 and 3 (1 has been removed) + // But errors property paths mismatch happening with "keep_as_list" option set to false + $errorPaths = [ + $errors[0]->getCause()->getPropertyPath(), + $errors[1]->getCause()->getPropertyPath(), + $errors[2]->getCause()->getPropertyPath(), + ]; + + $this->assertTrue($form->get('authors')->has('0')); + $this->assertContains('data.authors[0].firstName', $errorPaths); + + $this->assertFalse($form->get('authors')->has('1')); + $this->assertContains('data.authors[1].firstName', $errorPaths); + + $this->assertTrue($form->get('authors')->has('2')); + $this->assertContains('data.authors[2].firstName', $errorPaths); + + $this->assertTrue($form->get('authors')->has('3')); + $this->assertNotContains('data.authors[3].firstName', $errorPaths); + + // As result, root form contain errors + $this->assertCount(1, $form->getErrors(false)); + } + + public function testCollectionTypeKeepAsListOptionTrue() + { + $formMetadata = new ClassMetadata(Form::class); + $authorMetadata = (new ClassMetadata(Author::class)) + ->addPropertyConstraint('firstName', new NotBlank()); + $organizationMetadata = (new ClassMetadata(Organization::class)) + ->addPropertyConstraint('authors', new Valid()); + $metadataFactory = $this->createMock(MetadataFactoryInterface::class); + $metadataFactory->expects($this->any()) + ->method('getMetadataFor') + ->willReturnCallback(static function ($classOrObject) use ($formMetadata, $authorMetadata, $organizationMetadata) { + if (Author::class === $classOrObject || $classOrObject instanceof Author) { + return $authorMetadata; + } + + if (Organization::class === $classOrObject || $classOrObject instanceof Organization) { + return $organizationMetadata; + } + + if (Form::class === $classOrObject || $classOrObject instanceof Form) { + return $formMetadata; + } + + return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : $classOrObject::class); + }); + + $validator = Validation::createValidatorBuilder() + ->setMetadataFactory($metadataFactory) + ->getValidator(); + + $form = Forms::createFormFactoryBuilder() + ->addExtension(new ValidatorExtension($validator)) + ->getFormFactory() + ->create(FormTypeTest::TESTED_TYPE, new Organization([]), [ + 'data_class' => Organization::class, + 'by_reference' => false, + ]) + ->add('authors', CollectionTypeTest::TESTED_TYPE, [ + 'entry_type' => AuthorType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'keep_as_list' => true, + ]) + ; + + $form->submit([ + 'authors' => [ + 0 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName1', + ], + // key "1" could be missing if we add 4 blank form entries and then remove it. + 2 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName3', + ], + 3 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName3', + ], + ], + ]); + + // Form does have 3 not blank errors + $errors = $form->getErrors(true); + $this->assertCount(3, $errors); + + // No property paths mismatch happening with "keep_as_list" option set to true + $errorPaths = [ + $errors[0]->getCause()->getPropertyPath(), + $errors[1]->getCause()->getPropertyPath(), + $errors[2]->getCause()->getPropertyPath(), + ]; + + $this->assertTrue($form->get('authors')->has('0')); + $this->assertContains('data.authors[0].firstName', $errorPaths); + + $this->assertTrue($form->get('authors')->has('1')); + $this->assertContains('data.authors[1].firstName', $errorPaths); + + $this->assertTrue($form->get('authors')->has('2')); + $this->assertContains('data.authors[2].firstName', $errorPaths); + + $this->assertFalse($form->get('authors')->has('3')); + $this->assertNotContains('data.authors[3].firstName', $errorPaths); + + // Root form does NOT contain errors + $this->assertCount(0, $form->getErrors(false)); + } } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Organization.php b/src/Symfony/Component/Form/Tests/Fixtures/Organization.php new file mode 100644 index 0000000000000..db9cee9f96eeb --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/Organization.php @@ -0,0 +1,32 @@ +authors = $authors; + } + + public function getAuthors(): array + { + return $this->authors; + } + + public function addAuthor(Author $author): self + { + $this->authors[] = $author; + return $this; + } + + public function removeAuthor(Author $author): self + { + if (false !== $key = array_search($author, $this->authors, true)) { + array_splice($this->authors, $key, 1); + } + return $this; + } +} From a0e1d66d6bce7e1eeb700b331b2dc107ebabe61b Mon Sep 17 00:00:00 2001 From: Yassine Guedidi Date: Sat, 9 Dec 2023 12:03:36 +0100 Subject: [PATCH 040/395] Add IsCsrfTokenValid attribute --- .../Compiler/RegisterCsrfFeaturesPass.php | 5 + .../Bundle/SecurityBundle/composer.json | 2 +- .../Http/Attribute/IsCsrfTokenValid.php | 29 ++++ .../Component/Security/Http/CHANGELOG.md | 5 + .../IsCsrfTokenValidAttributeListener.php | 52 ++++++ .../IsCsrfTokenValidAttributeListenerTest.php | 154 ++++++++++++++++++ .../IsCsrfTokenValidAttributeController.php | 22 +++ ...rfTokenValidAttributeMethodsController.php | 36 ++++ 8 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Security/Http/Attribute/IsCsrfTokenValid.php create mode 100644 src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeController.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeMethodsController.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php index 20b79b07c49d2..5ee7c2268cc32 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php @@ -17,6 +17,7 @@ use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface; use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener; use Symfony\Component\Security\Http\EventListener\CsrfTokenClearingLogoutListener; +use Symfony\Component\Security\Http\EventListener\IsCsrfTokenValidAttributeListener; /** * @author Christian Flothmann @@ -41,6 +42,10 @@ private function registerCsrfProtectionListener(ContainerBuilder $container): vo $container->register('security.listener.csrf_protection', CsrfProtectionListener::class) ->addArgument(new Reference('security.csrf.token_manager')) ->addTag('kernel.event_subscriber'); + + $container->register('controller.is_csrf_token_valid_attribute_listener', IsCsrfTokenValidAttributeListener::class) + ->addArgument(new Reference('security.csrf.token_manager')) + ->addTag('kernel.event_subscriber'); } protected function registerLogoutHandler(ContainerBuilder $container): void diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index cc48593fc663a..0ae91f9cfb023 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -28,7 +28,7 @@ "symfony/password-hasher": "^6.4|^7.0", "symfony/security-core": "^6.4|^7.0", "symfony/security-csrf": "^6.4|^7.0", - "symfony/security-http": "^6.4|^7.0", + "symfony/security-http": "^7.1", "symfony/service-contracts": "^2.5|^3" }, "require-dev": { diff --git a/src/Symfony/Component/Security/Http/Attribute/IsCsrfTokenValid.php b/src/Symfony/Component/Security/Http/Attribute/IsCsrfTokenValid.php new file mode 100644 index 0000000000000..7cdd125473a35 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Attribute/IsCsrfTokenValid.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Attribute; + +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)] +final class IsCsrfTokenValid +{ + public function __construct( + /** + * Sets the id used when generating the token. + */ + public string $id, + + /** + * Sets the key of the request that contains the actual token value that should be validated. + */ + public ?string $tokenKey = '_token', + ) { + } +} diff --git a/src/Symfony/Component/Security/Http/CHANGELOG.md b/src/Symfony/Component/Security/Http/CHANGELOG.md index a33c980ac28a7..58f227f37383d 100644 --- a/src/Symfony/Component/Security/Http/CHANGELOG.md +++ b/src/Symfony/Component/Security/Http/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `#[IsCsrfTokenValid]` attribute + 7.0 --- diff --git a/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php b/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php new file mode 100644 index 0000000000000..0c24de1ad5da0 --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; + +/** + * Handles the IsCsrfTokenValid attribute on controllers. + */ +final class IsCsrfTokenValidAttributeListener implements EventSubscriberInterface +{ + public function __construct( + private readonly CsrfTokenManagerInterface $csrfTokenManager, + ) { + } + + public function onKernelControllerArguments(ControllerArgumentsEvent $event): void + { + /** @var IsCsrfTokenValid[] $attributes */ + if (!\is_array($attributes = $event->getAttributes()[IsCsrfTokenValid::class] ?? null)) { + return; + } + + $request = $event->getRequest(); + + foreach ($attributes as $attribute) { + if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($attribute->id, $request->request->getString($attribute->tokenKey)))) { + throw new InvalidCsrfTokenException('Invalid CSRF token.'); + } + } + } + + public static function getSubscribedEvents(): array + { + return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 25]]; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php new file mode 100644 index 0000000000000..e82748b65acde --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Http\EventListener\IsCsrfTokenValidAttributeListener; +use Symfony\Component\Security\Http\Tests\Fixtures\IsCsrfTokenValidAttributeController; +use Symfony\Component\Security\Http\Tests\Fixtures\IsCsrfTokenValidAttributeMethodsController; + +class IsCsrfTokenValidAttributeListenerTest extends TestCase +{ + public function testIsCsrfTokenValidCalledCorrectlyOnInvokableClass() + { + $request = new Request(request: ['_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 IsCsrfTokenValidAttributeController(), + [], + $request, + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } + + public function testNothingHappensWithNoConfig() + { + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->never()) + ->method('isTokenValid'); + + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + [new IsCsrfTokenValidAttributeMethodsController(), 'noAttribute'], + [], + new Request(), + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } + + public function testIsCsrfTokenValidCalledCorrectly() + { + $request = new Request(request: ['_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 testIsCsrfTokenValidCalledCorrectlyWithCustomTokenKey() + { + $request = new Request(request: ['my_token_key' => '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(), 'withCustomTokenKey'], + [], + $request, + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } + + public function testIsCsrfTokenValidCalledCorrectlyWithInvalidTokenKey() + { + $request = new Request(request: ['_token' => 'bar']); + + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->once()) + ->method('isTokenValid') + ->with(new CsrfToken('foo', '')) + ->willReturn(true); + + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + [new IsCsrfTokenValidAttributeMethodsController(), 'withInvalidTokenKey'], + [], + $request, + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } + + public function testExceptionWhenInvalidToken() + { + $this->expectException(InvalidCsrfTokenException::class); + + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->once()) + ->method('isTokenValid') + ->withAnyParameters() + ->willReturn(false); + + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + [new IsCsrfTokenValidAttributeMethodsController(), 'withDefaultTokenKey'], + [], + new Request(), + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeController.php b/src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeController.php new file mode 100644 index 0000000000000..4fa654239b675 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeController.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Fixtures; + +use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; + +#[IsCsrfTokenValid('foo')] +class IsCsrfTokenValidAttributeController +{ + public function __invoke() + { + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeMethodsController.php b/src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeMethodsController.php new file mode 100644 index 0000000000000..80d705cb50967 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeMethodsController.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Fixtures; + +use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; + +class IsCsrfTokenValidAttributeMethodsController +{ + public function noAttribute() + { + } + + #[IsCsrfTokenValid('foo')] + public function withDefaultTokenKey() + { + } + + #[IsCsrfTokenValid('foo', tokenKey: 'my_token_key')] + public function withCustomTokenKey() + { + } + + #[IsCsrfTokenValid('foo', tokenKey: 'invalid_token_key')] + public function withInvalidTokenKey() + { + } +} From 28d0b3f8eb2ec44a48806296401941e8a4fa4a8a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 10 Dec 2023 13:04:21 +0100 Subject: [PATCH 041/395] fix registering the Azure transport factory service --- .../FrameworkBundle/Resources/config/mailer_transports.php | 5 +++++ src/Symfony/Component/Mailer/Bridge/Azure/.gitignore | 1 + 2 files changed, 6 insertions(+) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php index 06c9632d80003..b8f8227384f9a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureTransportFactory; use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory; @@ -44,6 +45,10 @@ ->parent('mailer.transport_factory.abstract') ->tag('mailer.transport_factory') + ->set('mailer.transport_factory.azure', AzureTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + ->set('mailer.transport_factory.brevo', BrevoTransportFactory::class) ->parent('mailer.transport_factory.abstract') ->tag('mailer.transport_factory') diff --git a/src/Symfony/Component/Mailer/Bridge/Azure/.gitignore b/src/Symfony/Component/Mailer/Bridge/Azure/.gitignore index d1502b087b4d4..c49a5d8df5c65 100644 --- a/src/Symfony/Component/Mailer/Bridge/Azure/.gitignore +++ b/src/Symfony/Component/Mailer/Bridge/Azure/.gitignore @@ -1,2 +1,3 @@ vendor/ composer.lock +phpunit.xml From 4b201860da5eb4d4147419ff6bfbf0d2d74edadf Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Sun, 10 Dec 2023 10:00:39 +0100 Subject: [PATCH 042/395] [Messenger] Fix Redis integration tests data provider --- .../Redis/Tests/Transport/RedisExtIntegrationTest.php | 8 ++++---- .../Messenger/Bridge/Redis/Transport/Connection.php | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php index e9b9e80062657..0eb307eab0641 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php @@ -221,7 +221,7 @@ public function testConnectionClaimAndRedeliver() } /** - * @dataProvider + * @dataProvider sentinelOptionNames */ public function testSentinel(string $sentinelOptionName) { @@ -252,10 +252,10 @@ public function testSentinel(string $sentinelOptionName) $connection->cleanup(); } - public function sentinelOptionNames(): iterable + public static function sentinelOptionNames(): \Generator { - yield 'redis_sentinel'; - yield 'sentinel_master'; + yield ['redis_sentinel']; + yield ['sentinel_master']; } public function testLazySentinel() diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php index 471f88375dfd3..e1e786d644e48 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php @@ -47,6 +47,7 @@ class Connection 'auth' => null, 'serializer' => 1, // see \Redis::SERIALIZER_PHP, 'sentinel_master' => null, // String, master to look for (optional, default is NULL meaning Sentinel support is disabled) + 'redis_sentinel' => null, // String, alias for 'sentinel_master' 'timeout' => 0.0, // Float, value in seconds (optional, default is 0 meaning unlimited) 'read_timeout' => 0.0, // Float, value in seconds (optional, default is 0 meaning unlimited) 'retry_interval' => 0, // Int, value in milliseconds (optional, default is 0) From f92e6c3577c746360bde35382a65904b5d869178 Mon Sep 17 00:00:00 2001 From: valmonzo Date: Sat, 9 Dec 2023 13:45:16 +0100 Subject: [PATCH 043/395] [Messenger] Make `#[AsMessageHandler]` final --- UPGRADE-7.1.md | 5 +++++ .../Component/Messenger/Attribute/AsMessageHandler.php | 2 ++ src/Symfony/Component/Messenger/CHANGELOG.md | 1 + 3 files changed, 8 insertions(+) diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md index c0848e61e651e..e4200c3a24d22 100644 --- a/UPGRADE-7.1.md +++ b/UPGRADE-7.1.md @@ -1,6 +1,11 @@ UPGRADE FROM 7.0 to 7.1 ======================= +Messenger +--------- + + * Make `#[AsMessageHandler]` final + Workflow -------- diff --git a/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php b/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php index e0d764e5c4cb2..363553a799622 100644 --- a/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php +++ b/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php @@ -14,6 +14,8 @@ /** * Service tag to autoconfigure message handlers. * + * @final since Symfony 7.1 + * * @author Alireza Mirsepassi */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index 937a9fcb4dd8d..e741320def16e 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add option `redis_sentinel` as an alias for `sentinel_master` * Add `--all` option to the `messenger:consume` command + * Make `#[AsMessageHandler]` final 7.0 --- From 227e6eef3cdf5b7016a868f5f228516268d4a140 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 11 Dec 2023 08:59:31 +0100 Subject: [PATCH 044/395] allow Twig 4 --- src/Symfony/Bridge/Twig/composer.json | 2 +- src/Symfony/Bundle/FrameworkBundle/composer.json | 2 +- src/Symfony/Bundle/SecurityBundle/composer.json | 2 +- src/Symfony/Bundle/TwigBundle/composer.json | 2 +- src/Symfony/Bundle/WebProfilerBundle/composer.json | 2 +- src/Symfony/Component/HttpKernel/composer.json | 2 +- src/Symfony/Component/VarDumper/composer.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 4c32ee9e9a1b0..c4c0479a7ce16 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -18,7 +18,7 @@ "require": { "php": ">=8.2", "symfony/translation-contracts": "^2.5|^3", - "twig/twig": "^3.0.4" + "twig/twig": "^3.0.4|^4.0" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3|^4", diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 8e879fd213adc..e847cb1efd8cb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -71,7 +71,7 @@ "symfony/uid": "^6.4|^7.0", "symfony/web-link": "^6.4|^7.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "twig/twig": "^3.0.4" + "twig/twig": "^3.0.4|^4.0" }, "conflict": { "doctrine/persistence": "<1.3", diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 0ae91f9cfb023..38dc9ac251e2a 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -50,7 +50,7 @@ "symfony/twig-bridge": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0", - "twig/twig": "^3.0.4", + "twig/twig": "^3.0.4|^4.0", "web-token/jwt-checker": "^3.1", "web-token/jwt-signature-algorithm-hmac": "^3.1", "web-token/jwt-signature-algorithm-ecdsa": "^3.1", diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index 88c1dd5b85415..79e7d951c9169 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -23,7 +23,7 @@ "symfony/twig-bridge": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", - "twig/twig": "^3.0.4" + "twig/twig": "^3.0.4|^4.0" }, "require-dev": { "symfony/asset": "^6.4|^7.0", diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index 2de2677c5b0c3..a96f7fcb0057c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -22,7 +22,7 @@ "symfony/http-kernel": "^6.4|^7.0", "symfony/routing": "^6.4|^7.0", "symfony/twig-bundle": "^6.4|^7.0", - "twig/twig": "^3.0.4" + "twig/twig": "^3.0.4|^4.0" }, "require-dev": { "symfony/browser-kit": "^6.4|^7.0", diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 62d5f3eec7a56..7e967faca8f7f 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -45,7 +45,7 @@ "symfony/validator": "^6.4|^7.0", "symfony/var-exporter": "^6.4|^7.0", "psr/cache": "^1.0|^2.0|^3.0", - "twig/twig": "^3.0.4" + "twig/twig": "^3.0.4|^4.0" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" diff --git a/src/Symfony/Component/VarDumper/composer.json b/src/Symfony/Component/VarDumper/composer.json index cbc671760874d..eba8c966e19cb 100644 --- a/src/Symfony/Component/VarDumper/composer.json +++ b/src/Symfony/Component/VarDumper/composer.json @@ -25,7 +25,7 @@ "symfony/http-kernel": "^6.4|^7.0", "symfony/process": "^6.4|^7.0", "symfony/uid": "^6.4|^7.0", - "twig/twig": "^3.0.4" + "twig/twig": "^3.0.4|^4.0" }, "conflict": { "symfony/console": "<6.4" From 78ce055902ee52ec63b21df5ad1ab6dab37be752 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Sat, 9 Dec 2023 15:13:52 +0100 Subject: [PATCH 045/395] [Cache] Deprecate `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter` --- UPGRADE-7.1.md | 5 +++++ .../Cache/Adapter/CouchbaseBucketAdapter.php | 4 ++++ src/Symfony/Component/Cache/CHANGELOG.md | 1 + .../Adapter/CouchbaseBucketAdapterTest.php | 17 +++++++++-------- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md index e4200c3a24d22..cefaa966b352f 100644 --- a/UPGRADE-7.1.md +++ b/UPGRADE-7.1.md @@ -1,6 +1,11 @@ UPGRADE FROM 7.0 to 7.1 ======================= +Cache +----- + + * Deprecate `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter` instead + Messenger --------- diff --git a/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php b/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php index f8cb92dbf2fa2..3ba692f6b3da6 100644 --- a/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php @@ -16,8 +16,12 @@ use Symfony\Component\Cache\Marshaller\DefaultMarshaller; use Symfony\Component\Cache\Marshaller\MarshallerInterface; +trigger_deprecation('symfony/cache', '7.1', 'The "%s" class is deprecated, use "%s" instead.', CouchbaseBucketAdapter::class, CouchbaseCollectionAdapter::class); + /** * @author Antonio Jose Cerezo Aranda + * + * @deprecated since Symfony 7.1, use {@see CouchbaseCollectionAdapter} instead */ class CouchbaseBucketAdapter extends AbstractAdapter { diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index 69e8efb63483e..70ca8e3733963 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add option `sentinel_master` as an alias for `redis_sentinel` + * Deprecate `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter` 7.0 --- diff --git a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php index c596e66e12ea3..e51d391f970a5 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter; @@ -19,25 +20,25 @@ * @requires extension couchbase <3.0.0 * @requires extension couchbase >=2.6.0 * - * @group integration + * @group integration legacy * * @author Antonio Jose Cerezo Aranda */ class CouchbaseBucketAdapterTest extends AdapterTestCase { + use ExpectDeprecationTrait; + protected $skippedTests = [ 'testClearPrefix' => 'Couchbase cannot clear by prefix', ]; - protected static \CouchbaseBucket $client; + protected \CouchbaseBucket $client; - public static function setupBeforeClass(): void + protected function setUp(): void { - if (!CouchbaseBucketAdapter::isSupported()) { - self::markTestSkipped('Couchbase >= 2.6.0 < 3.0.0 is required.'); - } + $this->expectDeprecation('Since symfony/cache 7.1: The "Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter" class is deprecated, use "Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter" instead.'); - self::$client = AbstractAdapter::createConnection('couchbase://'.getenv('COUCHBASE_HOST').'/cache', + $this->client = AbstractAdapter::createConnection('couchbase://'.getenv('COUCHBASE_HOST').'/cache', ['username' => getenv('COUCHBASE_USER'), 'password' => getenv('COUCHBASE_PASS')] ); } @@ -50,7 +51,7 @@ public function createCachePool($defaultLifetime = 0): CacheItemPoolInterface .':'.getenv('COUCHBASE_PASS') .'@'.getenv('COUCHBASE_HOST') .'/cache') - : self::$client; + : $this->client; return new CouchbaseBucketAdapter($client, str_replace('\\', '.', __CLASS__), $defaultLifetime); } From 1f031f87427e51e865e9838a0e007cd4f1f59a6d Mon Sep 17 00:00:00 2001 From: Quentin Devos <4972091+Okhoshi@users.noreply.github.com> Date: Sat, 9 Dec 2023 11:58:51 +0100 Subject: [PATCH 046/395] [FrameworkBundle] Move Router cache directory to `kernel.build_dir` --- .../Bundle/FrameworkBundle/CHANGELOG.md | 6 +++ .../CacheWarmer/RouterCacheWarmer.php | 4 ++ .../DependencyInjection/Configuration.php | 5 ++- .../Bundle/FrameworkBundle/Routing/Router.php | 8 +++- .../CacheWarmer/RouterCacheWarmerTest.php | 43 +++++++++++++++---- .../DependencyInjection/ConfigurationTest.php | 2 +- 6 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index ed3b53d14ac42..4bde9c2f4a038 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +7.1 +--- + + * Move the Router `cache_dir` to `kernel.build_dir` + * Deprecate the `router.cache_dir` config option + 7.0 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php index 2af9a2fe80a3e..9dfa71c2c542f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php @@ -36,6 +36,10 @@ public function __construct(ContainerInterface $container) public function warmUp(string $cacheDir, string $buildDir = null): array { + if (!$buildDir) { + return []; + } + $router = $this->container->get('router'); if ($router instanceof WarmableInterface) { diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index a247418a9cd52..493835892395d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -613,7 +613,10 @@ private function addRouterSection(ArrayNodeDefinition $rootNode): void ->children() ->scalarNode('resource')->isRequired()->end() ->scalarNode('type')->end() - ->scalarNode('cache_dir')->defaultValue('%kernel.cache_dir%')->end() + ->scalarNode('cache_dir') + ->defaultValue('%kernel.build_dir%') + ->setDeprecated('symfony/framework-bundle', '7.1', 'Setting the "%path%.%node%" configuration option is deprecated. It will be removed in version 8.0.') + ->end() ->scalarNode('default_uri') ->info('The default URI used to generate URLs in a non-HTTP context') ->defaultNull() diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php index d6b1d57dc5b61..b5b7567de576c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php @@ -82,10 +82,14 @@ public function getRouteCollection(): RouteCollection public function warmUp(string $cacheDir, string $buildDir = null): array { + if (!$buildDir) { + return []; + } + $currentDir = $this->getOption('cache_dir'); - // force cache generation - $this->setOption('cache_dir', $cacheDir); + // force cache generation in build_dir + $this->setOption('cache_dir', $buildDir); $this->getMatcher(); $this->getGenerator(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php index 727b566e1ddb3..7686b139f28f9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php @@ -19,36 +19,61 @@ class RouterCacheWarmerTest extends TestCase { - public function testWarmUpWithWarmebleInterface() + public function testWarmUpWithWarmableInterfaceWithBuildDir() { $containerMock = $this->getMockBuilder(ContainerInterface::class)->onlyMethods(['get', 'has'])->getMock(); - $routerMock = $this->getMockBuilder(testRouterInterfaceWithWarmebleInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection', 'warmUp'])->getMock(); + $routerMock = $this->getMockBuilder(testRouterInterfaceWithWarmableInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection', 'warmUp'])->getMock(); $containerMock->expects($this->any())->method('get')->with('router')->willReturn($routerMock); $routerCacheWarmer = new RouterCacheWarmer($containerMock); - $routerCacheWarmer->warmUp('/tmp'); - $routerMock->expects($this->any())->method('warmUp')->with('/tmp')->willReturn([]); + $routerCacheWarmer->warmUp('/tmp/cache', '/tmp/build'); + $routerMock->expects($this->any())->method('warmUp')->with('/tmp/cache', '/tmp/build')->willReturn([]); $this->addToAssertionCount(1); } - public function testWarmUpWithoutWarmebleInterface() + public function testWarmUpWithoutWarmableInterfaceWithBuildDir() { $containerMock = $this->getMockBuilder(ContainerInterface::class)->onlyMethods(['get', 'has'])->getMock(); - $routerMock = $this->getMockBuilder(testRouterInterfaceWithoutWarmebleInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection'])->getMock(); + $routerMock = $this->getMockBuilder(testRouterInterfaceWithoutWarmableInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection'])->getMock(); $containerMock->expects($this->any())->method('get')->with('router')->willReturn($routerMock); $routerCacheWarmer = new RouterCacheWarmer($containerMock); $this->expectException(\LogicException::class); $this->expectExceptionMessage('cannot be warmed up because it does not implement "Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface"'); - $routerCacheWarmer->warmUp('/tmp'); + $routerCacheWarmer->warmUp('/tmp/cache', '/tmp/build'); + } + + public function testWarmUpWithWarmableInterfaceWithoutBuildDir() + { + $containerMock = $this->getMockBuilder(ContainerInterface::class)->onlyMethods(['get', 'has'])->getMock(); + + $routerMock = $this->getMockBuilder(testRouterInterfaceWithWarmableInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection', 'warmUp'])->getMock(); + $containerMock->expects($this->any())->method('get')->with('router')->willReturn($routerMock); + $routerCacheWarmer = new RouterCacheWarmer($containerMock); + + $preload = $routerCacheWarmer->warmUp('/tmp'); + $routerMock->expects($this->never())->method('warmUp'); + self::assertSame([], $preload); + $this->addToAssertionCount(1); + } + + public function testWarmUpWithoutWarmableInterfaceWithoutBuildDir() + { + $containerMock = $this->getMockBuilder(ContainerInterface::class)->onlyMethods(['get', 'has'])->getMock(); + + $routerMock = $this->getMockBuilder(testRouterInterfaceWithoutWarmableInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection'])->getMock(); + $containerMock->expects($this->any())->method('get')->with('router')->willReturn($routerMock); + $routerCacheWarmer = new RouterCacheWarmer($containerMock); + $preload = $routerCacheWarmer->warmUp('/tmp'); + self::assertSame([], $preload); } } -interface testRouterInterfaceWithWarmebleInterface extends RouterInterface, WarmableInterface +interface testRouterInterfaceWithWarmableInterface extends RouterInterface, WarmableInterface { } -interface testRouterInterfaceWithoutWarmebleInterface extends RouterInterface +interface testRouterInterfaceWithoutWarmableInterface extends RouterInterface { } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 82d9b354fe4c0..d56cfa90d7f48 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -634,7 +634,7 @@ protected static function getBundleDefaultConfig() 'https_port' => 443, 'strict_requirements' => true, 'utf8' => true, - 'cache_dir' => '%kernel.cache_dir%', + 'cache_dir' => '%kernel.build_dir%', ], 'session' => [ 'enabled' => false, From fde8cc9aa0f42f93e2ec36db40200dbb12501581 Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Sat, 9 Dec 2023 13:55:09 +0100 Subject: [PATCH 047/395] [HttpClient] Add `JsonMockResponse::fromFile()` and `MockResponse::fromFile()` shortcuts --- src/Symfony/Component/HttpClient/CHANGELOG.md | 1 + .../HttpClient/Response/JsonMockResponse.php | 14 +++++++++++++ .../HttpClient/Response/MockResponse.php | 9 ++++++++ .../Tests/Response/Fixtures/invalid_json.json | 1 + .../Tests/Response/Fixtures/response.json | 3 +++ .../Tests/Response/Fixtures/response.txt | 1 + .../Tests/Response/JsonMockResponseTest.php | 21 +++++++++++++++++++ .../Tests/Response/MockResponseTest.php | 12 ++++++++--- 8 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Component/HttpClient/Tests/Response/Fixtures/invalid_json.json create mode 100644 src/Symfony/Component/HttpClient/Tests/Response/Fixtures/response.json create mode 100644 src/Symfony/Component/HttpClient/Tests/Response/Fixtures/response.txt diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md index c9417a88315e7..4e9e09ee263e3 100644 --- a/src/Symfony/Component/HttpClient/CHANGELOG.md +++ b/src/Symfony/Component/HttpClient/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Allow mocking `start_time` info in `MockResponse` + * Add `MockResponse::fromFile()` and `JsonMockResponse::fromFile()` methods to help using fixtures files 7.0 --- diff --git a/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php b/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php index 66372aa8a8149..a0ef7d28b471b 100644 --- a/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/JsonMockResponse.php @@ -30,4 +30,18 @@ public function __construct(mixed $body = [], array $info = []) parent::__construct($json, $info); } + + public static function fromFile(string $path, array $info = []): static + { + if (!is_file($path)) { + throw new InvalidArgumentException(sprintf('File not found: "%s".', $path)); + } + + $json = file_get_contents($path); + if (!json_validate($json)) { + throw new \InvalidArgumentException(sprintf('File "%s" does not contain valid JSON.', $path)); + } + + return new static(json_decode($json, true, flags: \JSON_THROW_ON_ERROR), $info); + } } diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index ed2b2008f0c99..19041e3070ccd 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -64,6 +64,15 @@ public function __construct(string|iterable $body = '', array $info = []) self::addResponseHeaders($responseHeaders, $this->info, $this->headers); } + public static function fromFile(string $path, array $info = []): static + { + if (!is_file($path)) { + throw new \InvalidArgumentException(sprintf('File not found: "%s".', $path)); + } + + return new static(file_get_contents($path), $info); + } + /** * Returns the options used when doing the request. */ diff --git a/src/Symfony/Component/HttpClient/Tests/Response/Fixtures/invalid_json.json b/src/Symfony/Component/HttpClient/Tests/Response/Fixtures/invalid_json.json new file mode 100644 index 0000000000000..02ec6a9a01ade --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Response/Fixtures/invalid_json.json @@ -0,0 +1 @@ +foo ccc \ No newline at end of file diff --git a/src/Symfony/Component/HttpClient/Tests/Response/Fixtures/response.json b/src/Symfony/Component/HttpClient/Tests/Response/Fixtures/response.json new file mode 100644 index 0000000000000..c8c4105eb57cd --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Response/Fixtures/response.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} diff --git a/src/Symfony/Component/HttpClient/Tests/Response/Fixtures/response.txt b/src/Symfony/Component/HttpClient/Tests/Response/Fixtures/response.txt new file mode 100644 index 0000000000000..b978efc508aee --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Response/Fixtures/response.txt @@ -0,0 +1 @@ +foo bar ccc \ No newline at end of file diff --git a/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php b/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php index b371c08cf4241..bd4c404fa61ca 100644 --- a/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php +++ b/src/Symfony/Component/HttpClient/Tests/Response/JsonMockResponseTest.php @@ -85,4 +85,25 @@ public static function responseHeadersProvider(): array ['application/problem+json', ['x-foo' => 'ccc', 'content-type' => 'application/problem+json']], ]; } + + public function testFromFile() + { + $client = new MockHttpClient(JsonMockResponse::fromFile(__DIR__.'/Fixtures/response.json')); + $response = $client->request('GET', 'https://symfony.com'); + + $this->assertSame([ + 'foo' => 'bar', + ], $response->toArray()); + $this->assertSame('application/json', $response->getHeaders()['content-type'][0]); + } + + public function testFromFileWithInvalidJson() + { + $path = __DIR__.'/Fixtures/invalid_json.json'; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('File "%s" does not contain valid JSON.', $path)); + + JsonMockResponse::fromFile($path); + } } diff --git a/src/Symfony/Component/HttpClient/Tests/Response/MockResponseTest.php b/src/Symfony/Component/HttpClient/Tests/Response/MockResponseTest.php index 3051e29b4f03b..909b3dec8da0d 100644 --- a/src/Symfony/Component/HttpClient/Tests/Response/MockResponseTest.php +++ b/src/Symfony/Component/HttpClient/Tests/Response/MockResponseTest.php @@ -15,11 +15,9 @@ use Symfony\Component\HttpClient\Exception\InvalidArgumentException; use Symfony\Component\HttpClient\Exception\JsonException; use Symfony\Component\HttpClient\Exception\TransportException; +use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; -/** - * Test methods from Symfony\Component\HttpClient\Response\*ResponseTrait. - */ class MockResponseTest extends TestCase { public function testTotalTimeShouldBeSimulatedWhenNotProvided() @@ -133,4 +131,12 @@ public function testMustBeIssuedByMockHttpClient() (new MockResponse())->getContent(); } + + public function testFromFile() + { + $client = new MockHttpClient(MockResponse::fromFile(__DIR__.'/Fixtures/response.txt')); + $response = $client->request('GET', 'https://symfony.com'); + + $this->assertSame('foo bar ccc', $response->getContent()); + } } From 442329a50ff0bf4936f1868e2c758b6762dddbec Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 14 Dec 2023 11:03:37 +0100 Subject: [PATCH 048/395] Set `strict` parameter of `in_array` to true where possible --- .../Command/TranslationDebugCommand.php | 4 ++-- .../DependencyInjection/Compiler/UnusedTagsPass.php | 2 +- .../DependencyInjection/Configuration.php | 2 +- .../DependencyInjection/FrameworkExtension.php | 2 +- .../TwigBundle/DependencyInjection/Configuration.php | 2 +- src/Symfony/Component/Console/Application.php | 2 +- .../Console/Descriptor/ReStructuredTextDescriptor.php | 2 +- .../Component/Console/Helper/TableCellStyle.php | 2 +- src/Symfony/Component/CssSelector/Parser/Token.php | 2 +- .../Compiler/RegisterEnvVarProcessorsPass.php | 2 +- .../Compiler/ResolveInstanceofConditionalsPass.php | 2 +- src/Symfony/Component/Filesystem/Path.php | 2 +- .../Component/Filesystem/Tests/FilesystemTest.php | 2 +- .../Extension/Validator/Constraints/FormValidator.php | 2 +- src/Symfony/Component/Form/Test/TypeTestCase.php | 2 +- src/Symfony/Component/HttpClient/CurlHttpClient.php | 4 ++-- src/Symfony/Component/HttpFoundation/HeaderBag.php | 2 +- src/Symfony/Component/HttpFoundation/Request.php | 10 +++++----- src/Symfony/Component/HttpKernel/HttpCache/Esi.php | 2 +- .../Component/HttpKernel/HttpCache/HttpCache.php | 2 +- src/Symfony/Component/HttpKernel/HttpCache/Ssi.php | 2 +- .../HttpKernel/Tests/HttpCache/HttpCacheTest.php | 4 ++-- .../Intl/Data/Generator/RegionDataGenerator.php | 2 +- .../Bridge/Google/Transport/GmailTransportFactory.php | 2 +- .../Mailer/Transport/AbstractTransportFactory.php | 2 +- .../Component/Messenger/Handler/HandlersLocator.php | 2 +- .../Mime/Tests/Encoder/QpMimeHeaderEncoderTest.php | 2 +- src/Symfony/Component/Mime/Tests/Part/DataPartTest.php | 2 +- .../Bridge/LinkedIn/Share/LifecycleStateShare.php | 2 +- .../Bridge/LinkedIn/Share/ShareContentShare.php | 2 +- .../Notifier/Bridge/LinkedIn/Share/ShareMediaShare.php | 2 +- .../Notifier/Bridge/LinkedIn/Share/VisibilityShare.php | 4 ++-- .../MicrosoftTeams/Action/Input/MultiChoiceInput.php | 2 +- .../Bridge/MicrosoftTeams/Action/OpenUriAction.php | 2 +- .../Notifier/Bridge/Ntfy/NtfyTransportFactory.php | 2 +- .../Notifier/Bridge/Ntfy/Tests/NtfyTransportTest.php | 2 +- .../Notifier/Transport/AbstractTransportFactory.php | 2 +- .../PropertyInfo/Extractor/PhpDocExtractor.php | 2 +- .../PropertyInfo/Extractor/ReflectionExtractor.php | 6 +++--- src/Symfony/Component/PropertyInfo/Type.php | 2 +- .../Component/PropertyInfo/Util/PhpDocTypeHelper.php | 2 +- .../Component/PropertyInfo/Util/PhpStanTypeHelper.php | 4 ++-- .../Component/Routing/Matcher/TraceableUrlMatcher.php | 4 ++-- src/Symfony/Component/Routing/Matcher/UrlMatcher.php | 4 ++-- .../TraceableAccessDecisionManagerTest.php | 6 +++--- .../Component/Serializer/Mapping/AttributeMetadata.php | 2 +- .../CamelCaseToSnakeCaseNameConverter.php | 4 ++-- .../Serializer/Normalizer/AbstractNormalizer.php | 4 ++-- .../Serializer/Normalizer/AbstractObjectNormalizer.php | 2 +- .../Component/Translation/Bridge/Loco/LocoProvider.php | 2 +- .../Translation/Catalogue/AbstractOperation.php | 6 +++--- .../Component/Translation/Loader/PoFileLoader.php | 4 ++-- src/Symfony/Component/Translation/Translator.php | 2 +- .../Component/Uid/Command/GenerateUlidCommand.php | 2 +- .../Component/Uid/Command/GenerateUuidCommand.php | 2 +- src/Symfony/Component/Validator/Constraint.php | 2 +- .../Component/Validator/Constraints/CssColor.php | 2 +- src/Symfony/Component/Validator/Constraints/Length.php | 2 +- src/Symfony/Component/WebLink/GenericLinkProvider.php | 2 +- src/Symfony/Component/WebLink/Tests/LinkTest.php | 2 +- .../Component/Workflow/Dumper/MermaidDumper.php | 2 +- .../Component/Workflow/Validator/WorkflowValidator.php | 2 +- 62 files changed, 82 insertions(+), 82 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php index 79a67847a2ed7..15544a90c74f2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -223,8 +223,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - if (!\in_array(self::MESSAGE_UNUSED, $states) && $input->getOption('only-unused') - || !\in_array(self::MESSAGE_MISSING, $states) && $input->getOption('only-missing') + if (!\in_array(self::MESSAGE_UNUSED, $states, true) && $input->getOption('only-unused') + || !\in_array(self::MESSAGE_MISSING, $states, true) && $input->getOption('only-missing') ) { continue; } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 1d21c6b663688..6f53e57f069c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -110,7 +110,7 @@ public function process(ContainerBuilder $container): void foreach ($container->findUnusedTags() as $tag) { // skip known tags - if (\in_array($tag, self::KNOWN_TAGS)) { + if (\in_array($tag, self::KNOWN_TAGS, true)) { continue; } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 493835892395d..31dbc6f8d8c69 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -435,7 +435,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void if (!\is_string($value)) { return true; } - if (class_exists(WorkflowEvents::class) && !\in_array($value, WorkflowEvents::ALIASES)) { + if (class_exists(WorkflowEvents::class) && !\in_array($value, WorkflowEvents::ALIASES, true)) { return true; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d160942f60477..5ec5929bec168 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2179,7 +2179,7 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder ->setArguments([$transport['dsn'], $transport['options'] + ['transport_name' => $name], new Reference($serializerId)]) ->addTag('messenger.receiver', [ 'alias' => $name, - 'is_failure_transport' => \in_array($name, $failureTransports), + 'is_failure_transport' => \in_array($name, $failureTransports, true), ] ) ; diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index e5e3310eeddb5..ab6ceb2932f46 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -64,7 +64,7 @@ private function addFormThemesSection(ArrayNodeDefinition $rootNode): void ->prototype('scalar')->defaultValue('form_div_layout.html.twig')->end() ->example(['@My/form.html.twig']) ->validate() - ->ifTrue(fn ($v) => !\in_array('form_div_layout.html.twig', $v)) + ->ifTrue(fn ($v) => !\in_array('form_div_layout.html.twig', $v, true)) ->then(fn ($v) => array_merge(['form_div_layout.html.twig'], $v)) ->end() ->end() diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 07cc6d6749b48..4d1a6d95358d4 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -716,7 +716,7 @@ public function find(string $name): Command $aliases[$nameOrAlias] = $commandName; - return $commandName === $nameOrAlias || !\in_array($commandName, $commands); + return $commandName === $nameOrAlias || !\in_array($commandName, $commands, true); })); } diff --git a/src/Symfony/Component/Console/Descriptor/ReStructuredTextDescriptor.php b/src/Symfony/Component/Console/Descriptor/ReStructuredTextDescriptor.php index d4423fd3483ea..f12fecb67c404 100644 --- a/src/Symfony/Component/Console/Descriptor/ReStructuredTextDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/ReStructuredTextDescriptor.php @@ -226,7 +226,7 @@ private function getNonDefaultOptions(InputDefinition $definition): array $nonDefaultOptions = []; foreach ($definition->getOptions() as $option) { // Skip global options. - if (!\in_array($option->getName(), $globalOptions)) { + if (!\in_array($option->getName(), $globalOptions, true)) { $nonDefaultOptions[] = $option; } } diff --git a/src/Symfony/Component/Console/Helper/TableCellStyle.php b/src/Symfony/Component/Console/Helper/TableCellStyle.php index 9419dcb402e05..49b97f8539538 100644 --- a/src/Symfony/Component/Console/Helper/TableCellStyle.php +++ b/src/Symfony/Component/Console/Helper/TableCellStyle.php @@ -67,7 +67,7 @@ public function getTagOptions(): array { return array_filter( $this->getOptions(), - fn ($key) => \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]), + fn ($key) => \in_array($key, self::TAG_OPTIONS, true) && isset($this->options[$key]), \ARRAY_FILTER_USE_KEY ); } diff --git a/src/Symfony/Component/CssSelector/Parser/Token.php b/src/Symfony/Component/CssSelector/Parser/Token.php index b50441a8e611c..3e926c7f23dff 100644 --- a/src/Symfony/Component/CssSelector/Parser/Token.php +++ b/src/Symfony/Component/CssSelector/Parser/Token.php @@ -72,7 +72,7 @@ public function isDelimiter(array $values = []): bool return true; } - return \in_array($this->value, $values); + return \in_array($this->value, $values, true); } public function isWhitespace(): bool diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RegisterEnvVarProcessorsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RegisterEnvVarProcessorsPass.php index 0505455fe5367..4c562fbb49a4d 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RegisterEnvVarProcessorsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RegisterEnvVarProcessorsPass.php @@ -65,7 +65,7 @@ private static function validateProvidedTypes(string $types, string $class): arr $types = explode('|', $types); foreach ($types as $type) { - if (!\in_array($type, self::ALLOWED_TYPES)) { + if (!\in_array($type, self::ALLOWED_TYPES, true)) { throw new InvalidArgumentException(sprintf('Invalid type "%s" returned by "%s::getProvidedTypes()", expected one of "%s".', $type, $class, implode('", "', self::ALLOWED_TYPES))); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php index 442161ae0a120..31d943234d856 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php @@ -127,7 +127,7 @@ private function processDefinition(ContainerBuilder $container, string $id, Defi foreach ($tags as $k => $v) { if (null === $definition->getDecoratedService() || $interface === $definition->getClass() || \in_array($k, $tagsToKeep, true)) { foreach ($v as $v) { - if ($definition->hasTag($k) && \in_array($v, $definition->getTag($k))) { + if ($definition->hasTag($k) && \in_array($v, $definition->getTag($k), true)) { continue; } $definition->addTag($k, $v); diff --git a/src/Symfony/Component/Filesystem/Path.php b/src/Symfony/Component/Filesystem/Path.php index 6643962351feb..571cc7b70d4ea 100644 --- a/src/Symfony/Component/Filesystem/Path.php +++ b/src/Symfony/Component/Filesystem/Path.php @@ -668,7 +668,7 @@ public static function join(string ...$paths): string } // Only add slash if previous part didn't end with '/' or '\' - if (!\in_array(substr($finalPath, -1), ['/', '\\'])) { + if (!\in_array(substr($finalPath, -1), ['/', '\\'], true)) { $finalPath .= '/'; } diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index 2c222fd06b2db..dc7e74c849918 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -167,7 +167,7 @@ public function testCopyCreatesTargetDirectoryIfItDoesNotExist() */ public function testCopyForOriginUrlsAndExistingLocalFileDefaultsToCopy() { - if (!\in_array('https', stream_get_wrappers())) { + if (!\in_array('https', stream_get_wrappers(), true)) { $this->markTestSkipped('"https" stream wrapper is not enabled.'); } $sourceFilePath = 'https://symfony.com/images/common/logo/logo_symfony_header.png'; diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php index 4a05981a86eba..41635875761ca 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php +++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php @@ -120,7 +120,7 @@ public function validate(mixed $form, Constraint $formConstraint): void // Otherwise validate a constraint only once for the first // matching group foreach ($groups as $group) { - if (\in_array($group, $constraint->groups)) { + if (\in_array($group, $constraint->groups, true)) { $groupedConstraints[$group][] = $constraint; // Prevent duplicate validation diff --git a/src/Symfony/Component/Form/Test/TypeTestCase.php b/src/Symfony/Component/Form/Test/TypeTestCase.php index 5d4c2ba9c6228..960b44228ba5c 100644 --- a/src/Symfony/Component/Form/Test/TypeTestCase.php +++ b/src/Symfony/Component/Form/Test/TypeTestCase.php @@ -32,7 +32,7 @@ protected function getExtensions() { $extensions = []; - if (\in_array(ValidatorExtensionTrait::class, class_uses($this))) { + if (\in_array(ValidatorExtensionTrait::class, class_uses($this), true)) { $extensions[] = $this->getValidatorExtension(); } diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index bbaa4de28893c..4c64d5d66186d 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -532,11 +532,11 @@ private function validateExtraCurlOptions(array $options): void throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.', $constName, $curloptsToConfig[$opt])); } - if (\in_array($opt, $methodOpts)) { + if (\in_array($opt, $methodOpts, true)) { throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".'); } - if (\in_array($opt, $curloptsToCheck)) { + if (\in_array($opt, $curloptsToCheck, true)) { $constName = $this->findConstantName($opt) ?? $opt; throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".', $constName)); } diff --git a/src/Symfony/Component/HttpFoundation/HeaderBag.php b/src/Symfony/Component/HttpFoundation/HeaderBag.php index e26365ac4372e..40750d1c0910b 100644 --- a/src/Symfony/Component/HttpFoundation/HeaderBag.php +++ b/src/Symfony/Component/HttpFoundation/HeaderBag.php @@ -165,7 +165,7 @@ public function has(string $key): bool */ public function contains(string $key, string $value): bool { - return \in_array($value, $this->all($key)); + return \in_array($value, $this->all($key), true); } /** diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 89877051f1716..a31f1aace29d5 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -254,7 +254,7 @@ public static function createFromGlobals(): static $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER); if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded') - && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH']) + && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'], true) ) { parse_str($request->getContent(), $data); $request->request = new InputBag($data); @@ -1090,7 +1090,7 @@ public function getHost(): string if (\count(self::$trustedHostPatterns) > 0) { // to avoid host header injection attacks, you should provide a list of trusted host patterns - if (\in_array($host, self::$trustedHosts)) { + if (\in_array($host, self::$trustedHosts, true)) { return $host; } @@ -1221,10 +1221,10 @@ public function getFormat(?string $mimeType): ?string } foreach (static::$formats as $format => $mimeTypes) { - if (\in_array($mimeType, (array) $mimeTypes)) { + if (\in_array($mimeType, (array) $mimeTypes, true)) { return $format; } - if (null !== $canonicalMimeType && \in_array($canonicalMimeType, (array) $mimeTypes)) { + if (null !== $canonicalMimeType && \in_array($canonicalMimeType, (array) $mimeTypes, true)) { return $format; } } @@ -1541,7 +1541,7 @@ public function getPreferredLanguage(array $locales = null): ?string $extendedPreferredLanguages[] = $language; if (false !== $position = strpos($language, '_')) { $superLanguage = substr($language, 0, $position); - if (!\in_array($superLanguage, $preferredLanguages)) { + if (!\in_array($superLanguage, $preferredLanguages, true)) { $extendedPreferredLanguages[] = $superLanguage; } } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php index 1fe20cbf3753e..3c72ceee0853e 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php @@ -62,7 +62,7 @@ public function process(Request $request, Response $response): Response } $parts = explode(';', $type); - if (!\in_array($parts[0], $this->contentTypes)) { + if (!\in_array($parts[0], $this->contentTypes, true)) { return $response; } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index a560661afb4f9..654537110fefc 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -390,7 +390,7 @@ protected function validate(Request $request, Response $entry, bool $catch = fal // return the response and not the cache entry if the response is valid but not cached $etag = $response->getEtag(); - if ($etag && \in_array($etag, $requestEtags) && !\in_array($etag, $cachedEtags)) { + if ($etag && \in_array($etag, $requestEtags, true) && !\in_array($etag, $cachedEtags, true)) { return $response; } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php index f436fed749153..433a6a6ea1bc2 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php @@ -46,7 +46,7 @@ public function process(Request $request, Response $response): Response } $parts = explode(';', $type); - if (!\in_array($parts[0], $this->contentTypes)) { + if (!\in_array($parts[0], $this->contentTypes, true)) { return $response; } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php index 1a9424cb0d003..e843a1badeddf 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php @@ -264,7 +264,7 @@ public function testValidatesPrivateResponsesCachedOnTheClient() if ($request->cookies->has('authenticated')) { $response->headers->set('Cache-Control', 'private, no-store'); $response->setETag('"private tag"'); - if (\in_array('"private tag"', $etags)) { + if (\in_array('"private tag"', $etags, true)) { $response->setStatusCode(304); } else { $response->setStatusCode(200); @@ -274,7 +274,7 @@ public function testValidatesPrivateResponsesCachedOnTheClient() } else { $response->headers->set('Cache-Control', 'public'); $response->setETag('"public tag"'); - if (\in_array('"public tag"', $etags)) { + if (\in_array('"public tag"', $etags, true)) { $response->setStatusCode(304); } else { $response->setStatusCode(200); diff --git a/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php b/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php index b03f56614c1ed..745e074157974 100644 --- a/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php +++ b/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php @@ -242,7 +242,7 @@ private function generateAlpha2ToNumericMapping(ArrayAccessibleResourceBundle $m continue; } - if (\in_array($alias, self::WITHDRAWN_CODES)) { + if (\in_array($alias, self::WITHDRAWN_CODES, true)) { continue; } diff --git a/src/Symfony/Component/Mailer/Bridge/Google/Transport/GmailTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Google/Transport/GmailTransportFactory.php index 8a0bd5626699e..6bfa2ec74ed32 100644 --- a/src/Symfony/Component/Mailer/Bridge/Google/Transport/GmailTransportFactory.php +++ b/src/Symfony/Component/Mailer/Bridge/Google/Transport/GmailTransportFactory.php @@ -23,7 +23,7 @@ final class GmailTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { - if (\in_array($dsn->getScheme(), $this->getSupportedSchemes())) { + if (\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) { return new GmailSmtpTransport($this->getUser($dsn), $this->getPassword($dsn), $this->dispatcher, $this->logger); } diff --git a/src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php b/src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php index 7690c5f7e08ac..469841031a474 100644 --- a/src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php +++ b/src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php @@ -34,7 +34,7 @@ public function __construct(EventDispatcherInterface $dispatcher = null, HttpCli public function supports(Dsn $dsn): bool { - return \in_array($dsn->getScheme(), $this->getSupportedSchemes()); + return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true); } abstract protected function getSupportedSchemes(): array; diff --git a/src/Symfony/Component/Messenger/Handler/HandlersLocator.php b/src/Symfony/Component/Messenger/Handler/HandlersLocator.php index 6c5daf3a718af..71bf83ec6dd9f 100644 --- a/src/Symfony/Component/Messenger/Handler/HandlersLocator.php +++ b/src/Symfony/Component/Messenger/Handler/HandlersLocator.php @@ -47,7 +47,7 @@ public function getHandlers(Envelope $envelope): iterable } $name = $handlerDescriptor->getName(); - if (\in_array($name, $seen)) { + if (\in_array($name, $seen, true)) { continue; } diff --git a/src/Symfony/Component/Mime/Tests/Encoder/QpMimeHeaderEncoderTest.php b/src/Symfony/Component/Mime/Tests/Encoder/QpMimeHeaderEncoderTest.php index 34025a22fb2fc..544b22e9e09cd 100644 --- a/src/Symfony/Component/Mime/Tests/Encoder/QpMimeHeaderEncoderTest.php +++ b/src/Symfony/Component/Mime/Tests/Encoder/QpMimeHeaderEncoderTest.php @@ -99,7 +99,7 @@ public function testOnlyCharactersAllowedInPhrasesAreUsed() foreach (range(0x00, 0xFF) as $byte) { $char = pack('C', $byte); $encodedChar = $encoder->encodeString($char, 'iso-8859-1'); - if (\in_array($byte, $allowedBytes)) { + if (\in_array($byte, $allowedBytes, true)) { $this->assertEquals($char, $encodedChar, 'Character '.$char.' should not be encoded.'); } elseif (0x20 == $byte) { // special case diff --git a/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php b/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php index 6d9eb31b4fb9a..63e3ca49e1460 100644 --- a/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php +++ b/src/Symfony/Component/Mime/Tests/Part/DataPartTest.php @@ -139,7 +139,7 @@ public function testFromPathWithNotAFile() */ public function testFromPathWithUrl() { - if (!\in_array('https', stream_get_wrappers())) { + if (!\in_array('https', stream_get_wrappers(), true)) { $this->markTestSkipped('"https" stream wrapper is not enabled.'); } diff --git a/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/LifecycleStateShare.php b/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/LifecycleStateShare.php index 78f933164f9f2..55c156a89bb05 100644 --- a/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/LifecycleStateShare.php +++ b/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/LifecycleStateShare.php @@ -40,7 +40,7 @@ final class LifecycleStateShare extends AbstractLinkedInShare public function __construct(string $lifecycleState = self::PUBLISHED) { - if (!\in_array($lifecycleState, self::AVAILABLE_LIFECYCLE)) { + if (!\in_array($lifecycleState, self::AVAILABLE_LIFECYCLE, true)) { throw new LogicException(sprintf('"%s" is not a valid value, available lifecycle are "%s".', $lifecycleState, implode(', ', self::AVAILABLE_LIFECYCLE))); } diff --git a/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/ShareContentShare.php b/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/ShareContentShare.php index 1c70a6cde9273..ee0bc9e0d65ac 100644 --- a/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/ShareContentShare.php +++ b/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/ShareContentShare.php @@ -72,7 +72,7 @@ public function __construct(string $text, array $attributes = [], string $inferr } if ($shareMediaCategory) { - if (!\in_array($shareMediaCategory, self::ALL)) { + if (!\in_array($shareMediaCategory, self::ALL, true)) { throw new LogicException(sprintf('"%s" is not valid option, available options are "%s".', $shareMediaCategory, implode(', ', self::ALL))); } diff --git a/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/ShareMediaShare.php b/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/ShareMediaShare.php index f41fb85d45e3c..82dd3bcaf70e2 100644 --- a/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/ShareMediaShare.php +++ b/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/ShareMediaShare.php @@ -54,7 +54,7 @@ public function __construct(string $text, array $attributes = [], string $inferr } if (null !== $landingPageTitle) { - if (!\in_array($landingPageTitle, self::ALL)) { + if (!\in_array($landingPageTitle, self::ALL, true)) { throw new LogicException(sprintf('"%s" is not valid option, available options are "%s".', $landingPageTitle, implode(', ', self::ALL))); } diff --git a/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/VisibilityShare.php b/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/VisibilityShare.php index 03ca05bde25ba..cbf9b69ee8f14 100644 --- a/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/VisibilityShare.php +++ b/src/Symfony/Component/Notifier/Bridge/LinkedIn/Share/VisibilityShare.php @@ -39,11 +39,11 @@ final class VisibilityShare extends AbstractLinkedInShare public function __construct(string $visibility = self::MEMBER_NETWORK_VISIBILITY, string $value = 'PUBLIC') { - if (!\in_array($visibility, self::AVAILABLE_VISIBILITY)) { + if (!\in_array($visibility, self::AVAILABLE_VISIBILITY, true)) { throw new LogicException(sprintf('"%s" is not a valid visibility, available visibility are "%s".', $visibility, implode(', ', self::AVAILABLE_VISIBILITY))); } - if (self::MEMBER_NETWORK_VISIBILITY === $visibility && !\in_array($value, self::MEMBER_NETWORK)) { + if (self::MEMBER_NETWORK_VISIBILITY === $visibility && !\in_array($value, self::MEMBER_NETWORK, true)) { throw new LogicException(sprintf('"%s" is not a valid value, available value for visibility "%s" are "%s".', $value, $visibility, implode(', ', self::MEMBER_NETWORK))); } diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/MultiChoiceInput.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/MultiChoiceInput.php index 779ff74357a19..0781e82c4d47a 100644 --- a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/MultiChoiceInput.php +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/MultiChoiceInput.php @@ -53,7 +53,7 @@ public function isMultiSelect(bool $multiSelect): static */ public function style(string $style): static { - if (!\in_array($style, self::STYLES)) { + if (!\in_array($style, self::STYLES, true)) { throw new InvalidArgumentException(sprintf('Supported styles for "%s" method are: "%s".', __METHOD__, implode('", "', self::STYLES))); } diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/OpenUriAction.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/OpenUriAction.php index 0535af6aeaa9b..eeb95dba6ca54 100644 --- a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/OpenUriAction.php +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/OpenUriAction.php @@ -45,7 +45,7 @@ public function name(string $name): static */ public function target(string $uri, string $os = 'default'): static { - if (!\in_array($os, self::OPERATING_SYSTEMS)) { + if (!\in_array($os, self::OPERATING_SYSTEMS, true)) { throw new InvalidArgumentException(sprintf('Supported operating systems for "%s" method are: "%s".', __METHOD__, implode('", "', self::OPERATING_SYSTEMS))); } diff --git a/src/Symfony/Component/Notifier/Bridge/Ntfy/NtfyTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Ntfy/NtfyTransportFactory.php index b469090f8f0e0..b0f534e75a003 100644 --- a/src/Symfony/Component/Notifier/Bridge/Ntfy/NtfyTransportFactory.php +++ b/src/Symfony/Component/Notifier/Bridge/Ntfy/NtfyTransportFactory.php @@ -30,7 +30,7 @@ public function create(Dsn $dsn): TransportInterface $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); $topic = substr($dsn->getPath(), 1); - if (\in_array($dsn->getOption('secureHttp', true), [0, false, 'false', 'off', 'no'])) { + if (\in_array($dsn->getOption('secureHttp', true), [0, false, 'false', 'off', 'no'], true)) { $secureHttp = false; } else { $secureHttp = true; diff --git a/src/Symfony/Component/Notifier/Bridge/Ntfy/Tests/NtfyTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Ntfy/Tests/NtfyTransportTest.php index cb8485750c463..5d2782bb23187 100644 --- a/src/Symfony/Component/Notifier/Bridge/Ntfy/Tests/NtfyTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Ntfy/Tests/NtfyTransportTest.php @@ -99,7 +99,7 @@ public function testSendWithUserAndPassword() $expectedBody = json_encode(['topic' => 'test', 'title' => 'Hello', 'message' => 'World']); $expectedAuthorization = 'Authorization: Basic dGVzdF91c2VyOnRlc3RfcGFzc3dvcmQ'; $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); - $this->assertTrue(\in_array($expectedAuthorization, $options['headers'])); + $this->assertTrue(\in_array($expectedAuthorization, $options['headers'], true)); return $response; }); diff --git a/src/Symfony/Component/Notifier/Transport/AbstractTransportFactory.php b/src/Symfony/Component/Notifier/Transport/AbstractTransportFactory.php index a2115b8577f6d..acb703c6e07a3 100644 --- a/src/Symfony/Component/Notifier/Transport/AbstractTransportFactory.php +++ b/src/Symfony/Component/Notifier/Transport/AbstractTransportFactory.php @@ -32,7 +32,7 @@ public function __construct(EventDispatcherInterface $dispatcher = null, HttpCli public function supports(Dsn $dsn): bool { - return \in_array($dsn->getScheme(), $this->getSupportedSchemes()); + return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true); } /** diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php index ab056e12b0eba..8be810e6da958 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -158,7 +158,7 @@ public function getTypes(string $class, string $property, array $context = []): return null; } - if (!\in_array($prefix, $this->arrayMutatorPrefixes)) { + if (!\in_array($prefix, $this->arrayMutatorPrefixes, true)) { return $types; } diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index e6069e0bffe46..0d4829d0049aa 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -424,7 +424,7 @@ private function extractFromMutator(string $class, string $property): ?array } $type = $this->extractFromReflectionType($reflectionType, $reflectionMethod->getDeclaringClass()); - if (1 === \count($type) && \in_array($prefix, $this->arrayMutatorPrefixes)) { + if (1 === \count($type) && \in_array($prefix, $this->arrayMutatorPrefixes, true)) { $type = [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $type[0])]; } @@ -631,7 +631,7 @@ private function getMutatorMethod(string $class, string $property): ?array foreach ($mutatorPrefixes as $prefix) { $names = [$ucProperty]; - if (\in_array($prefix, $this->arrayMutatorPrefixes)) { + if (\in_array($prefix, $this->arrayMutatorPrefixes, true)) { $names = array_merge($names, $ucSingulars); } @@ -660,7 +660,7 @@ private function getPropertyName(string $methodName, array $reflectionProperties $pattern = implode('|', array_merge($this->accessorPrefixes, $this->mutatorPrefixes)); if ('' !== $pattern && preg_match('/^('.$pattern.')(.+)$/i', $methodName, $matches)) { - if (!\in_array($matches[1], $this->arrayMutatorPrefixes)) { + if (!\in_array($matches[1], $this->arrayMutatorPrefixes, true)) { return $matches[2]; } diff --git a/src/Symfony/Component/PropertyInfo/Type.php b/src/Symfony/Component/PropertyInfo/Type.php index daff8bd92b44e..0750ae6b4a8fc 100644 --- a/src/Symfony/Component/PropertyInfo/Type.php +++ b/src/Symfony/Component/PropertyInfo/Type.php @@ -78,7 +78,7 @@ class Type */ public function __construct(string $builtinType, bool $nullable = false, string $class = null, bool $collection = false, array|self $collectionKeyType = null, array|self $collectionValueType = null) { - if (!\in_array($builtinType, self::$builtinTypes)) { + if (!\in_array($builtinType, self::$builtinTypes, true)) { throw new \InvalidArgumentException(sprintf('"%s" is not a valid PHP type.', $builtinType)); } diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index 754b6ae5fc4ec..f56d64623360e 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -178,7 +178,7 @@ private function normalizeType(string $docType): string private function getPhpTypeAndClass(string $docType): array { - if (\in_array($docType, Type::$builtinTypes)) { + if (\in_array($docType, Type::$builtinTypes, true)) { return [$docType, null]; } diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php index 0a02071ec70b7..d9b1a7a3ccd31 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php @@ -165,8 +165,8 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array return [new Type(Type::BUILTIN_TYPE_OBJECT, false, $nameScope->resolveRootClass())]; } if ($node instanceof IdentifierTypeNode) { - if (\in_array($node->name, Type::$builtinTypes)) { - return [new Type($node->name, false, null, \in_array($node->name, Type::$builtinCollectionTypes))]; + if (\in_array($node->name, Type::$builtinTypes, true)) { + return [new Type($node->name, false, null, \in_array($node->name, Type::$builtinCollectionTypes, true))]; } return match ($node->name) { diff --git a/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php b/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php index 0f8970580a450..a5d871d83be22 100644 --- a/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php @@ -125,7 +125,7 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes): a } if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { - if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) { + if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods, true))) { $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route); return $this->allow = $this->allowSchemes = []; @@ -140,7 +140,7 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes): a continue; } - if ($requiredMethods && !\in_array($method, $requiredMethods)) { + if ($requiredMethods && !\in_array($method, $requiredMethods, true)) { $this->allow = array_merge($this->allow, $requiredMethods); $this->addTrace(sprintf('Method "%s" does not match any of the required methods (%s)', $this->context->getMethod(), implode(', ', $requiredMethods)), self::ROUTE_ALMOST_MATCHES, $name, $route); continue; diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index a341e52353b57..e60ff7ca6b191 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -163,7 +163,7 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes): a } if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { - if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) { + if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods, true))) { return $this->allow = $this->allowSchemes = []; } continue; @@ -174,7 +174,7 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes): a continue; } - if ($requiredMethods && !\in_array($method, $requiredMethods)) { + if ($requiredMethods && !\in_array($method, $requiredMethods, true)) { $this->allow = array_merge($this->allow, $requiredMethods); continue; } diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php index cefe8dbc1273a..c14ee1fa459d0 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php @@ -197,7 +197,7 @@ public function testAccessDecisionManagerCalledByVoter() ->expects($this->any()) ->method('vote') ->willReturnCallback(function (TokenInterface $token, $subject, array $attributes) use ($sut, $voter1) { - $vote = \in_array('attr1', $attributes) ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_ABSTAIN; + $vote = \in_array('attr1', $attributes, true) ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_ABSTAIN; $sut->addVoterVote($voter1, $attributes, $vote); return $vote; @@ -207,7 +207,7 @@ public function testAccessDecisionManagerCalledByVoter() ->expects($this->any()) ->method('vote') ->willReturnCallback(function (TokenInterface $token, $subject, array $attributes) use ($sut, $voter2) { - if (\in_array('attr2', $attributes)) { + if (\in_array('attr2', $attributes, true)) { $vote = null == $subject ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED; } else { $vote = VoterInterface::ACCESS_ABSTAIN; @@ -222,7 +222,7 @@ public function testAccessDecisionManagerCalledByVoter() ->expects($this->any()) ->method('vote') ->willReturnCallback(function (TokenInterface $token, $subject, array $attributes) use ($sut, $voter3) { - if (\in_array('attr2', $attributes) && $subject) { + if (\in_array('attr2', $attributes, true) && $subject) { $vote = $sut->decide($token, $attributes) ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED; } else { $vote = VoterInterface::ACCESS_ABSTAIN; diff --git a/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php b/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php index 9b04bb7e3757d..647d59309cd08 100644 --- a/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php +++ b/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php @@ -90,7 +90,7 @@ public function getName(): string public function addGroup(string $group): void { - if (!\in_array($group, $this->groups)) { + if (!\in_array($group, $this->groups, true)) { $this->groups[] = $group; } } diff --git a/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php b/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php index ab6f99e13e0eb..a7b450fd27a34 100644 --- a/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php +++ b/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php @@ -30,7 +30,7 @@ public function __construct( public function normalize(string $propertyName): string { - if (null === $this->attributes || \in_array($propertyName, $this->attributes)) { + if (null === $this->attributes || \in_array($propertyName, $this->attributes, true)) { return strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName))); } @@ -45,7 +45,7 @@ public function denormalize(string $propertyName): string $camelCasedName = lcfirst($camelCasedName); } - if (null === $this->attributes || \in_array($camelCasedName, $this->attributes)) { + if (null === $this->attributes || \in_array($camelCasedName, $this->attributes, true)) { return $camelCasedName; } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index f53d4b139b076..40945fd2d2e2b 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -253,7 +253,7 @@ protected function getGroups(array $context): array protected function isAllowedAttribute(object|string $classOrObject, string $attribute, string $format = null, array $context = []): bool { $ignoredAttributes = $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES]; - if (\in_array($attribute, $ignoredAttributes)) { + if (\in_array($attribute, $ignoredAttributes, true)) { return false; } @@ -326,7 +326,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex $attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context); $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; - $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes); + $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes, true); $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context); if ($constructorParameter->isVariadic()) { if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 7868ec10dd93c..f27161cf11ed2 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -334,7 +334,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $attributeContext = $this->getAttributeDenormalizationContext($resolvedClass, $attribute, $context); - if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass, $attribute, $format, $context)) { + if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes, true)) || !$this->isAllowedAttribute($resolvedClass, $attribute, $format, $context)) { if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) { $extraAttributes[] = $attribute; } diff --git a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php index c3b6d2267a6fe..e309c65e2141a 100644 --- a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php @@ -67,7 +67,7 @@ public function write(TranslatorBagInterface $translatorBag): void foreach ($translatorBag->getCatalogues() as $catalogue) { $locale = $catalogue->getLocale(); - if (!\in_array($locale, $this->getLocales())) { + if (!\in_array($locale, $this->getLocales(), true)) { $this->createLocale($locale); } diff --git a/src/Symfony/Component/Translation/Catalogue/AbstractOperation.php b/src/Symfony/Component/Translation/Catalogue/AbstractOperation.php index 559d3a5d22791..7f559a4a93dda 100644 --- a/src/Symfony/Component/Translation/Catalogue/AbstractOperation.php +++ b/src/Symfony/Component/Translation/Catalogue/AbstractOperation.php @@ -96,7 +96,7 @@ public function getDomains(): array public function getMessages(string $domain): array { - if (!\in_array($domain, $this->getDomains())) { + if (!\in_array($domain, $this->getDomains(), true)) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } @@ -109,7 +109,7 @@ public function getMessages(string $domain): array public function getNewMessages(string $domain): array { - if (!\in_array($domain, $this->getDomains())) { + if (!\in_array($domain, $this->getDomains(), true)) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } @@ -122,7 +122,7 @@ public function getNewMessages(string $domain): array public function getObsoleteMessages(string $domain): array { - if (!\in_array($domain, $this->getDomains())) { + if (!\in_array($domain, $this->getDomains(), true)) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } diff --git a/src/Symfony/Component/Translation/Loader/PoFileLoader.php b/src/Symfony/Component/Translation/Loader/PoFileLoader.php index 620d97339aed6..4f8aeb2cb8aeb 100644 --- a/src/Symfony/Component/Translation/Loader/PoFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/PoFileLoader.php @@ -76,7 +76,7 @@ protected function loadResource(string $resource): array if ('' === $line) { // Whitespace indicated current item is done - if (!\in_array('fuzzy', $flags)) { + if (!\in_array('fuzzy', $flags, true)) { $this->addMessage($messages, $item); } $item = $defaults; @@ -108,7 +108,7 @@ protected function loadResource(string $resource): array } } // save last item - if (!\in_array('fuzzy', $flags)) { + if (!\in_array('fuzzy', $flags, true)) { $this->addMessage($messages, $item); } fclose($stream); diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index 63037c574c0e7..30b3d6b82d3dc 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -112,7 +112,7 @@ public function addResource(string $format, mixed $resource, string $locale, str $this->resources[$locale][] = [$format, $resource, $domain]; - if (\in_array($locale, $this->fallbackLocales)) { + if (\in_array($locale, $this->fallbackLocales, true)) { $this->catalogues = []; } else { unset($this->catalogues[$locale]); diff --git a/src/Symfony/Component/Uid/Command/GenerateUlidCommand.php b/src/Symfony/Component/Uid/Command/GenerateUlidCommand.php index 596d8a08aaf54..6ae3153a40d4c 100644 --- a/src/Symfony/Component/Uid/Command/GenerateUlidCommand.php +++ b/src/Symfony/Component/Uid/Command/GenerateUlidCommand.php @@ -79,7 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $formatOption = $input->getOption('format'); - if (\in_array($formatOption, $this->getAvailableFormatOptions())) { + if (\in_array($formatOption, $this->getAvailableFormatOptions(), true)) { $format = 'to'.ucfirst($formatOption); } else { $io->error(sprintf('Invalid format "%s", supported formats are "%s".', $formatOption, implode('", "', $this->getAvailableFormatOptions()))); diff --git a/src/Symfony/Component/Uid/Command/GenerateUuidCommand.php b/src/Symfony/Component/Uid/Command/GenerateUuidCommand.php index 10924edf32af1..3f49baacfb66f 100644 --- a/src/Symfony/Component/Uid/Command/GenerateUuidCommand.php +++ b/src/Symfony/Component/Uid/Command/GenerateUuidCommand.php @@ -168,7 +168,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $formatOption = $input->getOption('format'); - if (\in_array($formatOption, $this->getAvailableFormatOptions())) { + if (\in_array($formatOption, $this->getAvailableFormatOptions(), true)) { $format = 'to'.ucfirst($formatOption); } else { $io->error(sprintf('Invalid format "%s", supported formats are "%s".', $formatOption, implode('", "', $this->getAvailableFormatOptions()))); diff --git a/src/Symfony/Component/Validator/Constraint.php b/src/Symfony/Component/Validator/Constraint.php index b6a47643bcb81..c8ec4041eadfe 100644 --- a/src/Symfony/Component/Validator/Constraint.php +++ b/src/Symfony/Component/Validator/Constraint.php @@ -228,7 +228,7 @@ public function addImplicitGroupName(string $group): void throw new \LogicException(sprintf('"%s::$groups" is set to null. Did you forget to call "%s::__construct()"?', static::class, self::class)); } - if (\in_array(self::DEFAULT_GROUP, $this->groups) && !\in_array($group, $this->groups)) { + if (\in_array(self::DEFAULT_GROUP, $this->groups) && !\in_array($group, $this->groups, true)) { $this->groups[] = $group; } } diff --git a/src/Symfony/Component/Validator/Constraints/CssColor.php b/src/Symfony/Component/Validator/Constraints/CssColor.php index 73b7c0543c41f..641a2aa2951f1 100644 --- a/src/Symfony/Component/Validator/Constraints/CssColor.php +++ b/src/Symfony/Component/Validator/Constraints/CssColor.php @@ -77,7 +77,7 @@ public function __construct(array|string $formats = [], string $message = null, $options['value'] = $formats; } elseif (\is_string($formats)) { - if (!\in_array($formats, self::$validationModes)) { + if (!\in_array($formats, self::$validationModes, true)) { throw new InvalidArgumentException(sprintf('The "formats" parameter value is not valid. It must contain one or more of the following values: "%s".', $validationModesAsString)); } diff --git a/src/Symfony/Component/Validator/Constraints/Length.php b/src/Symfony/Component/Validator/Constraints/Length.php index 33d62fe94f8e4..740bfe27a351a 100644 --- a/src/Symfony/Component/Validator/Constraints/Length.php +++ b/src/Symfony/Component/Validator/Constraints/Length.php @@ -107,7 +107,7 @@ public function __construct( throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); } - if (!\in_array($this->countUnit, self::VALID_COUNT_UNITS)) { + if (!\in_array($this->countUnit, self::VALID_COUNT_UNITS, true)) { throw new InvalidArgumentException(sprintf('The "countUnit" option must be one of the "%s"::COUNT_* constants ("%s" given).', __CLASS__, $this->countUnit)); } } diff --git a/src/Symfony/Component/WebLink/GenericLinkProvider.php b/src/Symfony/Component/WebLink/GenericLinkProvider.php index 3df2f981b533c..78c319dc6fa6d 100644 --- a/src/Symfony/Component/WebLink/GenericLinkProvider.php +++ b/src/Symfony/Component/WebLink/GenericLinkProvider.php @@ -45,7 +45,7 @@ public function getLinksByRel(string $rel): array $links = []; foreach ($this->links as $link) { - if (\in_array($rel, $link->getRels())) { + if (\in_array($rel, $link->getRels(), true)) { $links[] = $link; } } diff --git a/src/Symfony/Component/WebLink/Tests/LinkTest.php b/src/Symfony/Component/WebLink/Tests/LinkTest.php index e1c03bae7c073..226bc3af11620 100644 --- a/src/Symfony/Component/WebLink/Tests/LinkTest.php +++ b/src/Symfony/Component/WebLink/Tests/LinkTest.php @@ -45,7 +45,7 @@ public function testCanRemoveValues() ->withoutRel('next'); $this->assertEquals('http://www.google.com', $link->getHref()); - $this->assertFalse(\in_array('next', $link->getRels())); + $this->assertFalse(\in_array('next', $link->getRels(), true)); $this->assertArrayNotHasKey('me', $link->getAttributes()); } diff --git a/src/Symfony/Component/Workflow/Dumper/MermaidDumper.php b/src/Symfony/Component/Workflow/Dumper/MermaidDumper.php index 2d0f958f1324d..53436a1eca181 100644 --- a/src/Symfony/Component/Workflow/Dumper/MermaidDumper.php +++ b/src/Symfony/Component/Workflow/Dumper/MermaidDumper.php @@ -72,7 +72,7 @@ public function dump(Definition $definition, Marking $marking = null, array $opt $placeId, $place, $meta->getPlaceMetadata($place), - \in_array($place, $definition->getInitialPlaces()), + \in_array($place, $definition->getInitialPlaces(), true), $marking?->has($place) ?? false ); diff --git a/src/Symfony/Component/Workflow/Validator/WorkflowValidator.php b/src/Symfony/Component/Workflow/Validator/WorkflowValidator.php index 3f88d115c06b6..2afefb712a602 100644 --- a/src/Symfony/Component/Workflow/Validator/WorkflowValidator.php +++ b/src/Symfony/Component/Workflow/Validator/WorkflowValidator.php @@ -33,7 +33,7 @@ public function validate(Definition $definition, string $name): void $places = array_fill_keys($definition->getPlaces(), []); foreach ($definition->getTransitions() as $transition) { foreach ($transition->getFroms() as $from) { - if (\in_array($transition->getName(), $places[$from])) { + if (\in_array($transition->getName(), $places[$from], true)) { throw new InvalidDefinitionException(sprintf('All transitions for a place must have an unique name. Multiple transitions named "%s" where found for place "%s" in workflow "%s".', $transition->getName(), $from, $name)); } $places[$from][] = $transition->getName(); From a9d6f3cf43a74a9674f0884fb772b9d85f16ba7c Mon Sep 17 00:00:00 2001 From: Nyholm Date: Thu, 14 Dec 2023 19:51:27 +0100 Subject: [PATCH 049/395] [Notifier] Add Bluesky notifier bridge --- .../FrameworkExtension.php | 1 + .../Resources/config/notifier_transports.php | 4 + .../Notifier/Bridge/Bluesky/.gitattributes | 4 + .../Notifier/Bridge/Bluesky/.gitignore | 3 + .../Bridge/Bluesky/BlueskyTransport.php | 227 ++++++++++++++ .../Bluesky/BlueskyTransportFactory.php | 55 ++++ .../Notifier/Bridge/Bluesky/CHANGELOG.md | 7 + .../Component/Notifier/Bridge/Bluesky/LICENSE | 19 ++ .../Notifier/Bridge/Bluesky/README.md | 19 ++ .../Tests/BlueskyTransportFactoryTest.php | 48 +++ .../Bluesky/Tests/BlueskyTransportTest.php | 280 ++++++++++++++++++ .../Notifier/Bridge/Bluesky/composer.json | 36 +++ .../Notifier/Bridge/Bluesky/phpunit.xml.dist | 31 ++ 13 files changed, 734 insertions(+) create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/.gitattributes create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/.gitignore create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportFactoryTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/Bluesky/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d160942f60477..c25327f3d98be 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2695,6 +2695,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\AllMySms\AllMySmsTransportFactory::class => 'notifier.transport_factory.all-my-sms', NotifierBridge\AmazonSns\AmazonSnsTransportFactory::class => 'notifier.transport_factory.amazon-sns', NotifierBridge\Bandwidth\BandwidthTransportFactory::class => 'notifier.transport_factory.bandwidth', + NotifierBridge\Bluesky\BlueskyTransportFactory::class => 'notifier.transport_factory.bluesky', NotifierBridge\Brevo\BrevoTransportFactory::class => 'notifier.transport_factory.brevo', NotifierBridge\Chatwork\ChatworkTransportFactory::class => 'notifier.transport_factory.chatwork', NotifierBridge\Clickatell\ClickatellTransportFactory::class => 'notifier.transport_factory.clickatell', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 3feb1c080c623..f678e0588672f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -22,6 +22,10 @@ ->abstract() ->args([service('event_dispatcher'), service('http_client')->ignoreOnInvalid()]) + ->set('notifier.transport_factory.bluesky', Bridge\Bluesky\BlueskyTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + ->set('notifier.transport_factory.brevo', Bridge\Brevo\BrevoTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Bluesky/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/.gitignore b/src/Symfony/Component/Notifier/Bridge/Bluesky/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php b/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php new file mode 100644 index 0000000000000..dc307a9aea6be --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php @@ -0,0 +1,227 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Bluesky; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Component\String\AbstractString; +use Symfony\Component\String\ByteString; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Tobias Nyholm + */ +final class BlueskyTransport extends AbstractTransport +{ + private array $authSession = []; + + public function __construct( + #[\SensitiveParameter] + private string $user, + #[\SensitiveParameter] + private string $password, + private LoggerInterface $logger, + HttpClientInterface $client = null, + EventDispatcherInterface $dispatcher = null, + ) { + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('bluesky://%s', $this->getEndpoint()); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof ChatMessage; + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof ChatMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, ChatMessage::class, $message); + } + + if ([] === $this->authSession) { + $this->authenticate(); + } + + $post = [ + '$type' => 'app.bsky.feed.post', + 'text' => $message->getSubject(), + 'createdAt' => (new \DateTimeImmutable())->format('Y-m-d\\TH:i:s.u\\Z'), + ]; + if ([] !== $facets = $this->parseFacets($post['text'])) { + $post['facets'] = $facets; + } + + $response = $this->client->request('POST', sprintf('https://%s/xrpc/com.atproto.repo.createRecord', $this->getEndpoint()), [ + 'auth_bearer' => $this->authSession['accessJwt'] ?? null, + 'json' => [ + 'repo' => $this->authSession['did'] ?? null, + 'collection' => 'app.bsky.feed.post', + 'record' => $post, + ], + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote bluesky server.', $response, 0, $e); + } + + if (200 === $statusCode) { + $content = $response->toArray(); + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($content['cid']); + + return $sentMessage; + } + + try { + $content = $response->toArray(false); + } catch (DecodingExceptionInterface $e) { + throw new TransportException('Unexpected response from bluesky server.', $response, 0, $e); + } + + $title = $content['error'] ?? ''; + $errorDescription = $content['message'] ?? ''; + + throw new TransportException(sprintf('Unable to send message to Bluesky: Status code %d (%s) with message "%s".', $statusCode, $title, $errorDescription), $response); + } + + private function authenticate(): void + { + $response = $this->client->request('POST', sprintf('https://%s/xrpc/com.atproto.server.createSession', $this->getEndpoint()), [ + 'json' => [ + 'identifier' => $this->user, + 'password' => $this->password, + ], + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote bluesky server.', $response, 0, $e); + } + + if (200 !== $statusCode) { + throw new TransportException('Could not authenticate with the remote bluesky server.', $response); + } + + try { + $this->authSession = $response->toArray(false) ?? []; + } catch (DecodingExceptionInterface $e) { + throw new TransportException('Unexpected response from bluesky server.', $response, 0, $e); + } + } + + private function parseFacets(string $input): array + { + $facets = []; + $text = new ByteString($input); + + // regex based on: https://bluesky.com/specs/handle#handle-identifier-syntax + $regex = '#[$|\W](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)#'; + foreach ($this->getMatchAndPosition($text, $regex) as $match) { + $response = $this->client->request('GET', sprintf('https://%s/xrpc/com.atproto.identity.resolveHandle', $this->getEndpoint()), [ + 'query' => [ + 'handle' => ltrim($match['match'], '@'), + ], + ]); + try { + if (200 !== $response->getStatusCode()) { + continue; + } + } catch (TransportExceptionInterface $e) { + $this->logger->error('Could not reach the remote bluesky server. Tried to lookup username.', ['exception' => $e]); + throw $e; + } + + $did = $response->toArray(false)['did'] ?? null; + if (null === $did) { + $this->logger->error('Could not get a good response from bluesky server. Tried to lookup username.'); + continue; + } + + $facets[] = [ + 'index' => [ + 'byteStart' => $match['start'], + 'byteEnd' => $match['end'], + ], + 'features' => [ + [ + '$type' => 'app.bsky.richtext.facet#mention', + 'did' => $did, + ], + ], + ]; + } + + // partial/naive URL regex based on: https://stackoverflow.com/a/3809435 + // tweaked to disallow some trailing punctuation + $regex = ';[$|\W](https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?);'; + foreach ($this->getMatchAndPosition($text, $regex) as $match) { + $facets[] = [ + 'index' => [ + 'byteStart' => $match['start'], + 'byteEnd' => $match['end'], + ], + 'features' => [ + [ + '$type' => 'app.bsky.richtext.facet#link', + 'uri' => $match['match'], + ], + ], + ]; + } + + return $facets; + } + + private function getMatchAndPosition(AbstractString $text, string $regex): array + { + $output = []; + $handled = []; + $matches = $text->match($regex, \PREG_PATTERN_ORDER); + if ([] === $matches) { + return $output; + } + + $length = $text->length(); + foreach ($matches[1] as $match) { + if (isset($handled[$match])) { + continue; + } + $handled[$match] = true; + $end = -1; + while (null !== $start = $text->indexOf($match, min($length, $end + 1))) { + $output[] = [ + 'start' => $start, + 'end' => $end = $start + (new ByteString($match))->length(), + 'match' => $match, + ]; + } + } + + return $output; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransportFactory.php new file mode 100644 index 0000000000000..bf949ea6c56d3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransportFactory.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Bluesky; + +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Tobias Nyholm + */ +final class BlueskyTransportFactory extends AbstractTransportFactory +{ + public function __construct( + EventDispatcherInterface $dispatcher = null, + HttpClientInterface $client = null, + private ?LoggerInterface $logger = null + ) { + parent::__construct($dispatcher, $client); + } + + public function create(Dsn $dsn): BlueskyTransport + { + $scheme = $dsn->getScheme(); + + if ('bluesky' !== $scheme) { + throw new UnsupportedSchemeException($dsn, 'bluesky', $this->getSupportedSchemes()); + } + + $user = $this->getUser($dsn); + $secret = $this->getPassword($dsn); + + return (new BlueskyTransport($user, $secret, $this->logger ?? new NullLogger(), $this->client, $this->dispatcher)) + ->setHost($dsn->getHost()) + ->setPort($dsn->getPort()); + } + + protected function getSupportedSchemes(): array + { + return ['bluesky']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md new file mode 100644 index 0000000000000..5be39cbeeb951 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.1 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/LICENSE b/src/Symfony/Component/Notifier/Bridge/Bluesky/LICENSE new file mode 100644 index 0000000000000..3ed9f412ce53d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/README.md b/src/Symfony/Component/Notifier/Bridge/Bluesky/README.md new file mode 100644 index 0000000000000..72f5bb9000f58 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/README.md @@ -0,0 +1,19 @@ +Bluesky Notifier +================ + +Provides [Bluesky](https://bsky.app/) integration for Symfony Notifier. + +DSN example +----------- + +``` +BLUESKY_DSN=bluesky://nyholm.bsky.social:p4ssw0rd@bsky.social +``` + +Resources +--------- + + * [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/Notifier/Bridge/Bluesky/Tests/BlueskyTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportFactoryTest.php new file mode 100644 index 0000000000000..5f5b9a37ee47f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportFactoryTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Bluesky\Tests; + +use Symfony\Component\Notifier\Bridge\Bluesky\BlueskyTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; + +class BlueskyTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): BlueskyTransportFactory + { + return new BlueskyTransportFactory(); + } + + public static function createProvider(): iterable + { + yield [ + 'bluesky://bsky.social', + 'bluesky://user:pass@bsky.social', + ]; + } + + public static function supportsProvider(): iterable + { + yield [true, 'bluesky://foo:bar@bsky.social']; + yield [false, 'somethingElse://foo:bar@bsky.social']; + } + + public static function incompleteDsnProvider(): iterable + { + yield 'missing user and password token' => ['bluesky://host']; + yield 'missing password token' => ['bluesky://user@host']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://foo:bar@default']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php new file mode 100644 index 0000000000000..bcf0d04fa2e1d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php @@ -0,0 +1,280 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Bluesky\Tests; + +use Psr\Log\NullLogger; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\JsonMockResponse; +use Symfony\Component\Notifier\Bridge\Bluesky\BlueskyTransport; +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Transport\DummyMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +final class BlueskyTransportTest extends TransportTestCase +{ + public static function createTransport(HttpClientInterface $client = null): BlueskyTransport + { + $blueskyTransport = new BlueskyTransport('username', 'password', new NullLogger(), $client ?? new MockHttpClient()); + $blueskyTransport->setHost('bsky.social'); + + return $blueskyTransport; + } + + public static function toStringProvider(): iterable + { + yield ['bluesky://bsky.social', self::createTransport()]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new SmsMessage('+33612345678', 'Hello!')]; + yield [new DummyMessage()]; + } + + public function testExceptionIsThrownWhenNoMessageIsSent() + { + $transport = self::createTransport(); + + $this->expectException(LogicException::class); + $transport->send($this->createMock(MessageInterface::class)); + } + + /** + * Example from + * - https://atproto.com/blog/create-post + * - https://github.com/bluesky-social/atproto-website/blob/main/examples/create_bsky_post.py. + */ + public function testParseFacets() + { + $input = '✨ example mentioning @atproto.com the URL 👨‍❤️‍👨 https://en.wikipedia.org/wiki/CBOR.'; + $expected = + [ + [ + 'index' => ['byteStart' => 23, 'byteEnd' => 35], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#mention', 'did' => 'did=>plc=>ewvi7nxzyoun6zhxrhs64oiz'], + ], + ], + [ + 'index' => ['byteStart' => 65, 'byteEnd' => 99], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#link', 'uri' => 'https://en.wikipedia.org/wiki/CBOR'], + ], + ], + ]; + $output = $this->parseFacets($input, new MockHttpClient(new JsonMockResponse(['did' => 'did=>plc=>ewvi7nxzyoun6zhxrhs64oiz']))); + $this->assertEquals($expected, $output); + } + + /** + * Example from https://github.com/bluesky-social/atproto-website/blob/main/examples/create_bsky_post.py. + */ + public function testParseFacetsMultipleHandles() + { + $input = 'prefix @handle.example.com @handle.com suffix'; + $expected = [ + [ + 'index' => ['byteStart' => 7, 'byteEnd' => 26], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#mention', 'did' => 'did1'], + ], + ], + [ + 'index' => ['byteStart' => 27, 'byteEnd' => 38], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#mention', 'did' => 'did2'], + ], + ], + ]; + $output = $this->parseFacets($input, new MockHttpClient([new JsonMockResponse(['did' => 'did1']), new JsonMockResponse(['did' => 'did2'])])); + $this->assertEquals($expected, $output); + } + + /** + * Example from https://github.com/bluesky-social/atproto-website/blob/main/examples/create_bsky_post.py. + */ + public function testParseFacetsNoHandles() + { + $input = 'handle.example.com'; + $expected = []; + $output = $this->parseFacets($input, new MockHttpClient([new JsonMockResponse(['did' => 'no_value'])])); + $this->assertEquals($expected, $output); + } + + /** + * Example from https://github.com/bluesky-social/atproto-website/blob/main/examples/create_bsky_post.py. + */ + public function testParseFacetsInvalidHandle() + { + $input = '@bare'; + $expected = []; + $output = $this->parseFacets($input, new MockHttpClient([new JsonMockResponse(['did' => 'no_value'])])); + $this->assertEquals($expected, $output); + + $input = 'email@example.com'; + $expected = []; + $output = $this->parseFacets($input, new MockHttpClient([new JsonMockResponse(['did' => 'no_value'])])); + $this->assertEquals($expected, $output); + } + + /** + * Example from https://github.com/bluesky-social/atproto-website/blob/main/examples/create_bsky_post.py. + */ + public function testParseFacetsMentionWithEmoji() + { + $input = '💩💩💩 @handle.example.com'; + $expected = [ + [ + 'index' => ['byteStart' => 13, 'byteEnd' => 32], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#mention', 'did' => 'did0'], + ], + ], + ]; + $output = $this->parseFacets($input, new MockHttpClient([new JsonMockResponse(['did' => 'did0'])])); + $this->assertEquals($expected, $output); + } + + /** + * Example from https://github.com/bluesky-social/atproto-website/blob/main/examples/create_bsky_post.py. + */ + public function testParseFacetsWithEmail() + { + $input = 'cc:@example.com'; + $expected = [ + [ + 'index' => ['byteStart' => 3, 'byteEnd' => 15], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#mention', 'did' => 'did0'], + ], + ], + ]; + $output = $this->parseFacets($input, new MockHttpClient([new JsonMockResponse(['did' => 'did0'])])); + $this->assertEquals($expected, $output); + } + + /** + * Example from https://github.com/bluesky-social/atproto-website/blob/main/examples/create_bsky_post.py. + */ + public function testParseFacetsUrl() + { + $input = 'prefix https://example.com/index.html http://bsky.app suffix'; + $expected = [ + [ + 'index' => ['byteStart' => 7, 'byteEnd' => 37], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#link', 'uri' => 'https://example.com/index.html'], + ], + ], + [ + 'index' => ['byteStart' => 38, 'byteEnd' => 53], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#link', 'uri' => 'http://bsky.app'], + ], + ], + ]; + $output = $this->parseFacets($input); + $this->assertEquals($expected, $output); + } + + /** + * Example from https://github.com/bluesky-social/atproto-website/blob/main/examples/create_bsky_post.py. + */ + public function testParseFacetsNoUrls() + { + $input = 'example.com'; + $expected = []; + $output = $this->parseFacets($input); + $this->assertEquals($expected, $output); + + $input = 'runonhttp://blah.comcontinuesafter'; + $expected = []; + $output = $this->parseFacets($input); + $this->assertEquals($expected, $output); + } + + /** + * Example from https://github.com/bluesky-social/atproto-website/blob/main/examples/create_bsky_post.py. + */ + public function testParseFacetsUrlWithEmoji() + { + $input = '💩💩💩 http://bsky.app'; + $expected = [ + [ + 'index' => ['byteStart' => 13, 'byteEnd' => 28], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#link', 'uri' => 'http://bsky.app']], + ], + ]; + $output = $this->parseFacets($input); + $this->assertEquals($expected, $output); + } + + /** + * Example from https://github.com/bluesky-social/atproto-website/blob/main/examples/create_bsky_post.py. + */ + public function testParseFacetsUrlWithTrickyRegex() + { + $input = 'ref [https://bsky.app]'; + $expected = [ + [ + 'index' => ['byteStart' => 5, 'byteEnd' => 21], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#link', 'uri' => 'https://bsky.app']], + ], + ]; + $this->assertEquals($expected, $this->parseFacets($input)); + + $input = 'ref (https://bsky.app/)'; + $expected = [ + [ + 'index' => ['byteStart' => 5, 'byteEnd' => 22], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#link', 'uri' => 'https://bsky.app/']], + ], + ]; + $this->assertEquals($expected, $this->parseFacets($input)); + + $input = 'ends https://bsky.app. what else?'; + $expected = [ + [ + 'index' => ['byteStart' => 5, 'byteEnd' => 21], + 'features' => [ + ['$type' => 'app.bsky.richtext.facet#link', 'uri' => 'https://bsky.app']], + ], + ]; + $this->assertEquals($expected, $this->parseFacets($input)); + } + + /** + * A small helper function to test BlueskyTransport::parseFacets(). + */ + private function parseFacets(string $input, HttpClientInterface $httpClient = null): array + { + $class = new \ReflectionClass(BlueskyTransport::class); + $method = $class->getMethod('parseFacets'); + $method->setAccessible(true); + + $object = $class->newInstance('user', 'pass', new NullLogger(), $httpClient ?? new MockHttpClient([])); + + return $method->invoke($object, $input); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json b/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json new file mode 100644 index 0000000000000..453dd757bc574 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json @@ -0,0 +1,36 @@ +{ + "name": "symfony/bluesky-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony Bluesky Notifier Bridge", + "keywords": ["bluesky", "bluesky", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^7.1", + "symfony/string": "^6.4|^7.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Bluesky\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Bluesky/phpunit.xml.dist new file mode 100644 index 0000000000000..99623d7aefed3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Resources + ./Tests + ./vendor + + + From 8706f84c589f7b09c78549e85c376393c3f07b19 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 15 Dec 2023 13:46:43 -0500 Subject: [PATCH 050/395] [RateLimiter][FrameworkBundle] add `rate_limiter` tag to rate limiter services --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../FrameworkExtension.php | 3 ++- .../PhpFrameworkExtensionTest.php | 20 +++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 4bde9c2f4a038..ed156fda326fe 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Move the Router `cache_dir` to `kernel.build_dir` * Deprecate the `router.cache_dir` config option + * Add `rate_limiter` tags to rate limiter services 7.0 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 8776bcb9c362f..d8e061ad11fac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2860,7 +2860,8 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde // default configuration (when used by other DI extensions) $limiterConfig += ['lock_factory' => 'lock.factory', 'cache_pool' => 'cache.rate_limiter']; - $limiter = $container->setDefinition($limiterId = 'limiter.'.$name, new ChildDefinition('limiter')); + $limiter = $container->setDefinition($limiterId = 'limiter.'.$name, new ChildDefinition('limiter')) + ->addTag('rate_limiter', ['name' => $name]); if (null !== $limiterConfig['lock_factory']) { if (!interface_exists(LockInterface::class)) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index 53268ffd283d8..deac159b6f9b0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -245,4 +245,24 @@ public function testRateLimiterLockFactory() $container->getDefinition('limiter.without_lock')->getArgument(2); } + + public function testRateLimiterIsTagged() + { + $container = $this->createContainerFromClosure(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'lock' => true, + 'rate_limiter' => [ + 'first' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'], + 'second' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'], + ], + ]); + }); + + $this->assertSame('first', $container->getDefinition('limiter.first')->getTag('rate_limiter')[0]['name']); + $this->assertSame('second', $container->getDefinition('limiter.second')->getTag('rate_limiter')[0]['name']); + } } From 73dea2aa7536aec539e5b5a4a418d2af39d86637 Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Sat, 16 Dec 2023 00:21:18 +0300 Subject: [PATCH 051/395] [Notifier] Add Unifonic notifier bridge --- .../FrameworkExtension.php | 1 + .../Resources/config/notifier_transports.php | 4 + .../Notifier/Bridge/Unifonic/.gitattributes | 4 + .../Notifier/Bridge/Unifonic/.gitignore | 3 + .../Notifier/Bridge/Unifonic/CHANGELOG.md | 7 ++ .../Notifier/Bridge/Unifonic/LICENSE | 19 ++++ .../Notifier/Bridge/Unifonic/README.md | 19 ++++ .../Tests/UnifonicTransportFactoryTest.php | 53 +++++++++++ .../Unifonic/Tests/UnifonicTransportTest.php | 91 ++++++++++++++++++ .../Bridge/Unifonic/UnifonicTransport.php | 93 +++++++++++++++++++ .../Unifonic/UnifonicTransportFactory.php | 43 +++++++++ .../Notifier/Bridge/Unifonic/composer.json | 34 +++++++ .../Notifier/Bridge/Unifonic/phpunit.xml.dist | 30 ++++++ .../Exception/UnsupportedSchemeException.php | 4 + .../UnsupportedSchemeExceptionTest.php | 2 + 15 files changed, 407 insertions(+) create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/.gitattributes create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/.gitignore create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/Tests/UnifonicTransportFactoryTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/Tests/UnifonicTransportTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/UnifonicTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/UnifonicTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/Unifonic/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 8776bcb9c362f..3b611c38f0ed8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2760,6 +2760,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\TurboSms\TurboSmsTransport::class => 'notifier.transport_factory.turbo-sms', NotifierBridge\Twilio\TwilioTransportFactory::class => 'notifier.transport_factory.twilio', NotifierBridge\Twitter\TwitterTransportFactory::class => 'notifier.transport_factory.twitter', + NotifierBridge\Unifonic\UnifonicTransportFactory::class => 'notifier.transport_factory.unifonic', NotifierBridge\Vonage\VonageTransportFactory::class => 'notifier.transport_factory.vonage', NotifierBridge\Yunpian\YunpianTransportFactory::class => 'notifier.transport_factory.yunpian', NotifierBridge\Zendesk\ZendeskTransportFactory::class => 'notifier.transport_factory.zendesk', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index f678e0588672f..fd2952c50a9cd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -66,6 +66,10 @@ ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') + ->set('notifier.transport_factory.unifonic', Bridge\Unifonic\UnifonicTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ->set('notifier.transport_factory.all-my-sms', Bridge\AllMySms\AllMySmsTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Unifonic/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/.gitignore b/src/Symfony/Component/Notifier/Bridge/Unifonic/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Unifonic/CHANGELOG.md new file mode 100644 index 0000000000000..5be39cbeeb951 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.1 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/LICENSE b/src/Symfony/Component/Notifier/Bridge/Unifonic/LICENSE new file mode 100644 index 0000000000000..3ed9f412ce53d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/README.md b/src/Symfony/Component/Notifier/Bridge/Unifonic/README.md new file mode 100644 index 0000000000000..53902f9f12205 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/README.md @@ -0,0 +1,19 @@ +Unifonic Notifier +================ + +Provides [Unifonic](https://www.unifonic.com/) integration for Symfony Notifier. + +DSN example +----------- + +``` +UNIFONIC_DSN=unifonic://APP_SID@default?from={SENDER} +``` + +Resources +--------- + + * [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/Notifier/Bridge/Unifonic/Tests/UnifonicTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Unifonic/Tests/UnifonicTransportFactoryTest.php new file mode 100644 index 0000000000000..081f47a0c71ce --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/Tests/UnifonicTransportFactoryTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Unifonic\Tests; + +use Symfony\Component\Notifier\Bridge\Unifonic\UnifonicTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; + +final class UnifonicTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): UnifonicTransportFactory + { + return new UnifonicTransportFactory(); + } + + public static function createProvider(): iterable + { + yield [ + 'unifonic://host.test?from=Sender', + 'unifonic://s3cr3t@host.test?from=Sender', + ]; + yield [ + 'unifonic://host.test', + 'unifonic://s3cr3t@host.test', + ]; + } + + public static function supportsProvider(): iterable + { + yield [true, 'unifonic://host.test?from=Sender']; + yield [true, 'unifonic://default?from=Sender']; + yield [false, 'somethingElse://host.test?from=Sender']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://host.test?from=Sender']; + yield ['somethingElse://s3cr3t@host.test?from=Sender']; + } + + public static function incompleteDsnProvider(): iterable + { + yield ['unifonic://host.test', 'Invalid "unifonic://host.test" notifier DSN: User is not set.']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/Tests/UnifonicTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Unifonic/Tests/UnifonicTransportTest.php new file mode 100644 index 0000000000000..454c5d108beaf --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/Tests/UnifonicTransportTest.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Unifonic\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\JsonMockResponse; +use Symfony\Component\Notifier\Bridge\Unifonic\UnifonicTransport; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Transport\DummyMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class UnifonicTransportTest extends TransportTestCase +{ + public static function createTransport(HttpClientInterface $client = null, string $host = null): UnifonicTransport + { + return (new UnifonicTransport('S3cr3t', 'Sender', $client ?? new MockHttpClient()))->setHost($host); + } + + public static function toStringProvider(): iterable + { + yield ['unifonic://el.cloud.unifonic.com?from=Sender', self::createTransport()]; + yield ['unifonic://api.unifonic.com?from=Sender', self::createTransport(host: 'api.unifonic.com')]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + yield [new DummyMessage()]; + } + + public function testSendFailedByStatusCode() + { + $client = new MockHttpClient(static fn (): ResponseInterface => new JsonMockResponse(info: [ + 'http_code' => 400, + ])); + + $transport = self::createTransport($client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Unable to send SMS'); + + $transport->send(new SmsMessage('0611223344', 'Hello!')); + } + + public function testSendFailed() + { + $client = new MockHttpClient(static fn (): ResponseInterface => new JsonMockResponse([ + 'success' => false, + 'errorCode' => 'ER-123', + 'message' => 'Lorem Ipsum', + ])); + + $transport = self::createTransport($client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Unable to send the SMS. Reason: "Lorem Ipsum". Error code: "ER-123".'); + + $transport->send(new SmsMessage('0611223344', 'Hello!')); + } + + public function testSendSuccess() + { + $client = new MockHttpClient(static fn (): ResponseInterface => new JsonMockResponse([ + 'success' => true, + ])); + + $transport = self::createTransport($client, host: 'localhost'); + $sentMessage = $transport->send(new SmsMessage('0611223344', 'Hello!')); + + $this->assertSame('unifonic://localhost?from=Sender', $sentMessage->getTransport()); + $this->assertSame('Hello!', $sentMessage->getOriginalMessage()->getSubject()); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/UnifonicTransport.php b/src/Symfony/Component/Notifier/Bridge/Unifonic/UnifonicTransport.php new file mode 100644 index 0000000000000..7e7723a4f80f2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/UnifonicTransport.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Unifonic; + +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Farhad Safarov + */ +final class UnifonicTransport extends AbstractTransport +{ + protected const HOST = 'el.cloud.unifonic.com'; + + public function __construct( + #[\SensitiveParameter] + private readonly string $appSid, + private readonly ?string $from = null, + HttpClientInterface $client = null, + EventDispatcherInterface $dispatcher = null, + ) { + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('unifonic://%s%s', $this->getEndpoint(), null !== $this->from ? '?from='.$this->from : ''); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof SmsMessage; + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof SmsMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, SmsMessage::class, $message); + } + + $endpoint = sprintf('https://%s/rest/SMS/messages', $this->getEndpoint()); + + $body = [ + 'AppSid' => $this->appSid, + 'Body' => $message->getSubject(), + 'Recipient' => $message->getPhone(), + ]; + + if ('' !== $message->getFrom()) { + $body['SenderID'] = $message->getFrom(); + } elseif (null !== $this->from) { + $body['SenderID'] = $this->from; + } + + $response = $this->client->request('POST', $endpoint, [ + 'body' => $body, + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException(sprintf('Could not reach "%s" endpoint.', $endpoint), $response, previous: $e); + } + + if (200 !== $statusCode) { + throw new TransportException('Unable to send SMS.', $response); + } + + $content = $response->toArray(false); + + if ('true' != $content['success']) { + throw new TransportException(sprintf('Unable to send the SMS. Reason: "%s". Error code: "%s".', $content['message'], $content['errorCode']), $response); + } + + return new SentMessage($message, (string) $this); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/UnifonicTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Unifonic/UnifonicTransportFactory.php new file mode 100644 index 0000000000000..ee9845f6c65fd --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/UnifonicTransportFactory.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Unifonic; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Farhad Safarov + */ +final class UnifonicTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): UnifonicTransport + { + if ('unifonic' !== $dsn->getScheme()) { + throw new UnsupportedSchemeException($dsn, 'unifonic', $this->getSupportedSchemes()); + } + + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + + return (new UnifonicTransport( + $this->getUser($dsn), + $dsn->getOption('from'), + $this->client, + $this->dispatcher, + ))->setHost($host)->setPort($dsn->getPort()); + } + + protected function getSupportedSchemes(): array + { + return ['unifonic']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/composer.json b/src/Symfony/Component/Notifier/Bridge/Unifonic/composer.json new file mode 100644 index 0000000000000..6d1abb380b7d8 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/composer.json @@ -0,0 +1,34 @@ +{ + "name": "symfony/unifonic-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony Unifonic Notifier Bridge", + "keywords": ["unifonic", "sms", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Farhad Safarov", + "email": "farhad.safarov@gmail.com" + } + ], + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Unifonic\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Unifonic/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Unifonic/phpunit.xml.dist new file mode 100644 index 0000000000000..92bdf6bb4d2c8 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Unifonic/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Tests + ./vendor + + + diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index 1b80a86473263..f0ea7a49603e1 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -280,6 +280,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Twitter\TwitterTransportFactory::class, 'package' => 'symfony/twitter-notifier', ], + 'unifonic' => [ + 'class' => Bridge\Unifonic\UnifonicTransportFactory::class, + 'package' => 'symfony/unifonic-notifier', + ], 'vonage' => [ 'class' => Bridge\Vonage\VonageTransportFactory::class, 'package' => 'symfony/vonage-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index 1170d06cc4234..94a1291154231 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -91,6 +91,7 @@ public static function setUpBeforeClass(): void Bridge\TurboSms\TurboSmsTransportFactory::class => false, Bridge\Twilio\TwilioTransportFactory::class => false, Bridge\Twitter\TwitterTransportFactory::class => false, + Bridge\Unifonic\UnifonicTransportFactory::class => false, Bridge\Vonage\VonageTransportFactory::class => false, Bridge\Yunpian\YunpianTransportFactory::class => false, Bridge\Zendesk\ZendeskTransportFactory::class => false, @@ -169,6 +170,7 @@ public static function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \ yield ['turbosms', 'symfony/turbo-sms-notifier']; yield ['twilio', 'symfony/twilio-notifier']; yield ['twitter', 'symfony/twitter-notifier']; + yield ['unifonic', 'symfony/unifonic-notifier']; yield ['zendesk', 'symfony/zendesk-notifier']; yield ['zulip', 'symfony/zulip-notifier']; yield ['goip', 'symfony/go-ip-notifier']; From 3d4d3557f79b9114773855f286f85e573b019cef Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 8 Dec 2023 16:21:59 +0100 Subject: [PATCH 052/395] Use faster hashing algorithms when possible --- .../TranslationDefaultDomainNodeVisitor.php | 2 +- .../EventListener/ConsoleProfilerListener.php | 2 +- .../FrameworkExtensionTestCase.php | 2 +- .../CompleteConfigurationTestCase.php | 4 +- .../DependencyInjection/CachePoolPass.php | 2 +- .../DependencyInjection/CachePoolPassTest.php | 10 ++-- .../DependencyInjection/ContainerBuilder.php | 2 +- .../LazyProxy/PhpDumper/LazyServiceDumper.php | 2 +- .../RegisterServiceSubscribersPassTest.php | 2 +- .../Fixtures/php/lazy_autowire_attribute.php | 4 +- ...y_autowire_attribute_with_intersection.php | 12 ++-- .../php/services9_lazy_inlined_factories.txt | 8 +-- .../Fixtures/php/services_dedup_lazy.php | 12 ++-- .../php/services_non_shared_duplicates.php | 2 +- .../php/services_non_shared_lazy_ghost.php | 4 +- .../php/services_non_shared_lazy_public.php | 4 +- .../Tests/Fixtures/php/services_rot13_env.php | 2 +- .../php/services_service_locator_argument.php | 2 +- .../Fixtures/php/services_subscriber.php | 6 +- .../Fixtures/php/services_wither_lazy.php | 4 +- .../php/services_wither_lazy_non_shared.php | 4 +- .../DomCrawler/Field/FileFormField.php | 2 +- .../HttpFoundation/BinaryFileResponse.php | 2 +- .../Storage/MockArraySessionStorage.php | 2 +- .../Debug/TraceableEventDispatcher.php | 2 +- .../HttpKernel/Profiler/Profiler.php | 2 +- .../Crowdin/Tests/CrowdinProviderTest.php | 22 +++---- .../Phrase/Tests/PhraseProviderTest.php | 8 +-- .../Translation/Dumper/XliffFileDumper.php | 4 +- .../Command/TranslationPullCommandTest.php | 60 +++++++++---------- .../Tests/Fixtures/resources-2.0+intl-icu.xlf | 2 +- .../Tests/Fixtures/resources-2.0-clean.xlf | 8 +-- .../Tests/Fixtures/resources-clean.xlf | 6 +- .../Tests/Fixtures/resources-clean.xliff | 6 +- .../Tests/Fixtures/resources-notes-meta.xlf | 4 +- .../Fixtures/resources-target-attributes.xlf | 2 +- .../Tests/Fixtures/resources-tool-info.xlf | 2 +- .../Component/Translation/Translator.php | 2 +- 38 files changed, 114 insertions(+), 114 deletions(-) diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php index d0e3337a9239c..6b023138755f7 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php @@ -114,6 +114,6 @@ private function isNamedArguments(Node $arguments): bool private function getVarName(): string { - return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false)); + return sprintf('__internal_%s', hash('xxh128', uniqid(mt_rand(), true))); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php b/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php index d3fc3810631b6..19f0794d4927d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php +++ b/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php @@ -72,7 +72,7 @@ public function initialize(ConsoleCommandEvent $event): void return; } - $request->attributes->set('_stopwatch_token', substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); + $request->attributes->set('_stopwatch_token', substr(hash('xxh128', uniqid(mt_rand(), true)), 0, 6)); $this->stopwatch->openSection(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 460c899e377bc..4fa2ab34c2060 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -1693,7 +1693,7 @@ public function testCachePoolServices() ->replaceArgument(0, $expectedSeed) ->replaceArgument(1, 12), (new ChildDefinition('cache.adapter.redis')) - ->replaceArgument(0, new Reference('.cache_connection.kYdiLgf')) + ->replaceArgument(0, new Reference('.cache_connection.U5HliuY')) ->replaceArgument(1, $expectedSeed) ->replaceArgument(2, 12), ], diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php index ea01daa96bf73..858f99e748635 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php @@ -137,7 +137,7 @@ public function testFirewalls() [ 'simple', 'security.user_checker', - '.security.request_matcher.h5ibf38', + '.security.request_matcher.rud_2nr', false, false, '', @@ -187,7 +187,7 @@ public function testFirewalls() [ 'host', 'security.user_checker', - '.security.request_matcher.bcmu4fb', + '.security.request_matcher.ap9sh8g', true, false, 'security.user.provider.concrete.default', diff --git a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php index e29e9e2989eff..49d0ba32b7900 100644 --- a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php +++ b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php @@ -217,7 +217,7 @@ public function process(ContainerBuilder $container): void private function getNamespace(string $seed, string $id): string { - return substr(str_replace('/', '-', base64_encode(hash('sha256', $id.$seed, true))), 0, 10); + return substr(str_replace('/', '-', base64_encode(hash('xxh128', $id.$seed, true))), 0, 10); } /** diff --git a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php index 18647fb283cdf..98a093ed0222f 100644 --- a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php +++ b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php @@ -49,7 +49,7 @@ public function testNamespaceArgumentIsReplaced() $this->cachePoolPass->process($container); - $this->assertSame('z3X945Jbf5', $cachePool->getArgument(0)); + $this->assertSame('cKLcR15Llk', $cachePool->getArgument(0)); } public function testNamespaceArgumentIsSeededWithAdapterClassName() @@ -70,7 +70,7 @@ public function testNamespaceArgumentIsSeededWithAdapterClassName() $this->cachePoolPass->process($container); - $this->assertSame('xmOJ8gqF-Y', $cachePool->getArgument(0)); + $this->assertSame('mVXLns1cYU', $cachePool->getArgument(0)); } public function testNamespaceArgumentIsSeededWithAdapterClassNameWithoutAffectingOtherCachePools() @@ -97,7 +97,7 @@ public function testNamespaceArgumentIsSeededWithAdapterClassNameWithoutAffectin $this->cachePoolPass->process($container); - $this->assertSame('xmOJ8gqF-Y', $cachePool->getArgument(0)); + $this->assertSame('mVXLns1cYU', $cachePool->getArgument(0)); } public function testNamespaceArgumentIsNotReplacedIfArrayAdapterIsUsed() @@ -153,7 +153,7 @@ public function testArgsAreReplaced() $this->assertInstanceOf(Reference::class, $cachePool->getArgument(0)); $this->assertSame('foobar', (string) $cachePool->getArgument(0)); - $this->assertSame('6Ridbw4aMn', $cachePool->getArgument(1)); + $this->assertSame('ZmalVIjCbI', $cachePool->getArgument(1)); $this->assertSame(3, $cachePool->getArgument(2)); } @@ -174,7 +174,7 @@ public function testWithNameAttribute() $this->cachePoolPass->process($container); - $this->assertSame('PeXBWSl6ca', $cachePool->getArgument(1)); + $this->assertSame('5SvqAqqNBH', $cachePool->getArgument(1)); } public function testThrowsExceptionWhenCachePoolTagHasUnknownAttributes() diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 4f29e6a5d1b9a..a5942a0b8effc 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -1592,7 +1592,7 @@ public static function getInitializedConditionals(mixed $value): array */ public static function hash(mixed $value): string { - $hash = substr(base64_encode(hash('sha256', serialize($value), true)), 0, 7); + $hash = substr(base64_encode(hash('xxh128', serialize($value), true)), 0, 7); return str_replace(['/', '+'], ['.', '_'], $hash); } diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php index 31cef8d5f9895..282353916a970 100644 --- a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php @@ -146,6 +146,6 @@ public function getProxyClass(Definition $definition, bool $asGhostObject, \Refl return preg_replace('/^.*\\\\/', '', $definition->getClass()) .($asGhostObject ? 'Ghost' : 'Proxy') - .ucfirst(substr(hash('sha256', $this->salt.'+'.$class->name.'+'.serialize($definition->getTag('proxy'))), -7)); + .ucfirst(substr(hash('xxh128', $this->salt.'+'.$class->name.'+'.serialize($definition->getTag('proxy'))), -7)); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index b5e2458c337e3..0d943f46151e2 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -462,7 +462,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.oO4rxCy.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)), + 'autowire.decorated' => new ServiceClosureArgument(new Reference('.service_locator.420ES7z.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/Fixtures/php/lazy_autowire_attribute.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute.php index 8134075865d25..950c28ae12f0a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute.php @@ -78,14 +78,14 @@ protected static function getFooService($container) protected static function getFoo2Service($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = $container->createProxy('FooProxy4048957', static fn () => \FooProxy4048957::createLazyProxy(static fn () => self::getFoo2Service($container, false))); + return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = $container->createProxy('FooProxyCd8d23a', static fn () => \FooProxyCd8d23a::createLazyProxy(static fn () => self::getFoo2Service($container, false))); } return ($container->services['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()); } } -class FooProxy4048957 extends \Symfony\Component\DependencyInjection\Tests\Compiler\Foo implements \Symfony\Component\VarExporter\LazyObjectInterface +class FooProxyCd8d23a extends \Symfony\Component\DependencyInjection\Tests\Compiler\Foo implements \Symfony\Component\VarExporter\LazyObjectInterface { use \Symfony\Component\VarExporter\LazyProxyTrait; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php index 8dc0eb50e62fb..d09a2133b70e5 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php @@ -39,7 +39,7 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.lazy.foo.gDmfket' => true, + '.lazy.foo.qFdMZVK' => true, ]; } @@ -55,7 +55,7 @@ protected function createProxy($class, \Closure $factory) */ protected static function getFooService($container) { - $a = ($container->privates['.lazy.foo.gDmfket'] ?? self::get_Lazy_Foo_GDmfketService($container)); + $a = ($container->privates['.lazy.foo.qFdMZVK'] ?? self::get_Lazy_Foo_QFdMZVKService($container)); if (isset($container->services['foo'])) { return $container->services['foo']; @@ -65,21 +65,21 @@ protected static function getFooService($container) } /** - * Gets the private '.lazy.foo.gDmfket' shared service. + * Gets the private '.lazy.foo.qFdMZVK' shared service. * * @return \object */ - protected static function get_Lazy_Foo_GDmfketService($container, $lazyLoad = true) + protected static function get_Lazy_Foo_QFdMZVKService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->privates['.lazy.foo.gDmfket'] = $container->createProxy('objectProxy8ac8e9a', static fn () => \objectProxy8ac8e9a::createLazyProxy(static fn () => self::get_Lazy_Foo_GDmfketService($container, false))); + return $container->privates['.lazy.foo.qFdMZVK'] = $container->createProxy('objectProxy1fd6daa', static fn () => \objectProxy1fd6daa::createLazyProxy(static fn () => self::get_Lazy_Foo_QFdMZVKService($container, false))); } return ($container->services['foo'] ?? self::getFooService($container)); } } -class objectProxy8ac8e9a implements \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface, \Symfony\Component\DependencyInjection\Tests\Compiler\IInterface, \Symfony\Component\VarExporter\LazyObjectInterface +class objectProxy1fd6daa implements \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface, \Symfony\Component\DependencyInjection\Tests\Compiler\IInterface, \Symfony\Component\VarExporter\LazyObjectInterface { use \Symfony\Component\VarExporter\LazyProxyTrait; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_lazy_inlined_factories.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_lazy_inlined_factories.txt index 28a641d76222b..84a981bcca22d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_lazy_inlined_factories.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_lazy_inlined_factories.txt @@ -6,11 +6,11 @@ namespace Container%s; include_once $container->targetDir.''.'/Fixtures/includes/foo.php'; -class FooClassGhostEe53b95 extends \Bar\FooClass implements \Symfony\Component\VarExporter\LazyObjectInterface +class FooClassGhost1728205 extends \Bar\FooClass implements \Symfony\Component\VarExporter\LazyObjectInterface %A -if (!\class_exists('FooClassGhostEe53b95', false)) { - \class_alias(__NAMESPACE__.'\\FooClassGhostEe53b95', 'FooClassGhostEe53b95', false); +if (!\class_exists('FooClassGhost1728205', false)) { + \class_alias(__NAMESPACE__.'\\FooClassGhost1728205', 'FooClassGhost1728205', false); } [Container%s/ProjectServiceContainer.php] => services['lazy_foo'] = $container->createProxy('FooClassGhostEe53b95', static fn () => \FooClassGhostEe53b95::createLazyGhost(static fn ($proxy) => self::getLazyFooService($container, $proxy))); + return $container->services['lazy_foo'] = $container->createProxy('FooClassGhost1728205', static fn () => \FooClassGhost1728205::createLazyGhost(static fn ($proxy) => self::getLazyFooService($container, $proxy))); } include_once $container->targetDir.''.'/Fixtures/includes/foo_lazy.php'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy.php index 006820f527fd8..60add492ba1cd 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy.php @@ -52,7 +52,7 @@ protected function createProxy($class, \Closure $factory) protected static function getBarService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['bar'] = $container->createProxy('stdClassGhost2fc7938', static fn () => \stdClassGhost2fc7938::createLazyGhost(static fn ($proxy) => self::getBarService($container, $proxy))); + return $container->services['bar'] = $container->createProxy('stdClassGhostAa01f12', static fn () => \stdClassGhostAa01f12::createLazyGhost(static fn ($proxy) => self::getBarService($container, $proxy))); } return $lazyLoad; @@ -66,7 +66,7 @@ protected static function getBarService($container, $lazyLoad = true) protected static function getBazService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['baz'] = $container->createProxy('stdClassProxy2fc7938', static fn () => \stdClassProxy2fc7938::createLazyProxy(static fn () => self::getBazService($container, false))); + return $container->services['baz'] = $container->createProxy('stdClassProxyAa01f12', static fn () => \stdClassProxyAa01f12::createLazyProxy(static fn () => self::getBazService($container, false))); } return \foo_bar(); @@ -80,7 +80,7 @@ protected static function getBazService($container, $lazyLoad = true) protected static function getBuzService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['buz'] = $container->createProxy('stdClassProxy2fc7938', static fn () => \stdClassProxy2fc7938::createLazyProxy(static fn () => self::getBuzService($container, false))); + return $container->services['buz'] = $container->createProxy('stdClassProxyAa01f12', static fn () => \stdClassProxyAa01f12::createLazyProxy(static fn () => self::getBuzService($container, false))); } return \foo_bar(); @@ -94,14 +94,14 @@ protected static function getBuzService($container, $lazyLoad = true) protected static function getFooService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['foo'] = $container->createProxy('stdClassGhost2fc7938', static fn () => \stdClassGhost2fc7938::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); + return $container->services['foo'] = $container->createProxy('stdClassGhostAa01f12', static fn () => \stdClassGhostAa01f12::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); } return $lazyLoad; } } -class stdClassGhost2fc7938 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface +class stdClassGhostAa01f12 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface { use \Symfony\Component\VarExporter\LazyGhostTrait; @@ -113,7 +113,7 @@ class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); -class stdClassProxy2fc7938 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface +class stdClassProxyAa01f12 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface { use \Symfony\Component\VarExporter\LazyProxyTrait; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_duplicates.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_duplicates.php index d3685cf9d9e00..913d2ab4d829f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_duplicates.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_duplicates.php @@ -41,7 +41,7 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.service_locator.mtT6G8y' => true, + '.service_locator.lViPm9k' => true, 'foo' => true, ]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_ghost.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_ghost.php index 0082641c56ed0..b03463295309e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_ghost.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_ghost.php @@ -68,14 +68,14 @@ protected static function getFooService($container, $lazyLoad = true) $container->factories['service_container']['foo'] ??= self::getFooService(...); if (true === $lazyLoad) { - return $container->createProxy('stdClassGhost2fc7938', static fn () => \stdClassGhost2fc7938::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); + return $container->createProxy('stdClassGhostAa01f12', static fn () => \stdClassGhostAa01f12::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); } return $lazyLoad; } } -class stdClassGhost2fc7938 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface +class stdClassGhostAa01f12 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface { use \Symfony\Component\VarExporter\LazyGhostTrait; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_public.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_public.php index dbf0f5d5e1134..7f870f886abcb 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_public.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_public.php @@ -51,7 +51,7 @@ protected static function getFooService($container, $lazyLoad = true) $container->factories['foo'] ??= fn () => self::getFooService($container); if (true === $lazyLoad) { - return $container->createProxy('FooLazyClassGhost2108fce', static fn () => \FooLazyClassGhost2108fce::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); + return $container->createProxy('FooLazyClassGhost82ad1a4', static fn () => \FooLazyClassGhost82ad1a4::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); } static $include = true; @@ -66,7 +66,7 @@ protected static function getFooService($container, $lazyLoad = true) } } -class FooLazyClassGhost2108fce extends \Bar\FooLazyClass implements \Symfony\Component\VarExporter\LazyObjectInterface +class FooLazyClassGhost82ad1a4 extends \Bar\FooLazyClass implements \Symfony\Component\VarExporter\LazyObjectInterface { use \Symfony\Component\VarExporter\LazyGhostTrait; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php index a092759862e15..130d73c8240e7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php @@ -43,7 +43,7 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.service_locator.PWbaRiJ' => true, + '.service_locator.DyWBOhJ' => true, ]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php index 83e8d3a3a75b3..963f7ea10306e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php @@ -44,7 +44,7 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.service_locator.ZP1tNYN' => true, + '.service_locator.X7o4UPP' => true, 'foo2' => true, 'foo3' => true, 'foo4' => true, 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 0565bd68ce279..67242fe119f95 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php @@ -43,9 +43,9 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.service_locator.2hyyc9y' => true, - '.service_locator.KGUGnmw' => true, - '.service_locator.KGUGnmw.foo_service' => true, + '.service_locator.2x56Fsq' => true, + '.service_locator.2x56Fsq.foo_service' => true, + '.service_locator.K8KBCZO' => true, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true, ]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php index f52f226597625..b2940c88569f4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php @@ -56,7 +56,7 @@ protected function createProxy($class, \Closure $factory) protected static function getWitherService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['wither'] = $container->createProxy('WitherProxy580fe0f', static fn () => \WitherProxy580fe0f::createLazyProxy(static fn () => self::getWitherService($container, false))); + return $container->services['wither'] = $container->createProxy('WitherProxy1991f2a', static fn () => \WitherProxy1991f2a::createLazyProxy(static fn () => self::getWitherService($container, false))); } $instance = new \Symfony\Component\DependencyInjection\Tests\Compiler\Wither(); @@ -71,7 +71,7 @@ protected static function getWitherService($container, $lazyLoad = true) } } -class WitherProxy580fe0f extends \Symfony\Component\DependencyInjection\Tests\Compiler\Wither implements \Symfony\Component\VarExporter\LazyObjectInterface +class WitherProxy1991f2a extends \Symfony\Component\DependencyInjection\Tests\Compiler\Wither implements \Symfony\Component\VarExporter\LazyObjectInterface { use \Symfony\Component\VarExporter\LazyProxyTrait; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php index 0867347a6f845..0df7e0c98e274 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php @@ -58,7 +58,7 @@ protected static function getWitherService($container, $lazyLoad = true) $container->factories['wither'] ??= fn () => self::getWitherService($container); if (true === $lazyLoad) { - return $container->createProxy('WitherProxyDd381be', static fn () => \WitherProxyDd381be::createLazyProxy(static fn () => self::getWitherService($container, false))); + return $container->createProxy('WitherProxyE94fdba', static fn () => \WitherProxyE94fdba::createLazyProxy(static fn () => self::getWitherService($container, false))); } $instance = new \Symfony\Component\DependencyInjection\Tests\Compiler\Wither(); @@ -73,7 +73,7 @@ protected static function getWitherService($container, $lazyLoad = true) } } -class WitherProxyDd381be extends \Symfony\Component\DependencyInjection\Tests\Compiler\Wither implements \Symfony\Component\VarExporter\LazyObjectInterface +class WitherProxyE94fdba extends \Symfony\Component\DependencyInjection\Tests\Compiler\Wither implements \Symfony\Component\VarExporter\LazyObjectInterface { use \Symfony\Component\VarExporter\LazyProxyTrait; diff --git a/src/Symfony/Component/DomCrawler/Field/FileFormField.php b/src/Symfony/Component/DomCrawler/Field/FileFormField.php index a52ffcb28b6c6..e125a3d906c44 100644 --- a/src/Symfony/Component/DomCrawler/Field/FileFormField.php +++ b/src/Symfony/Component/DomCrawler/Field/FileFormField.php @@ -55,7 +55,7 @@ public function setValue(?string $value): void $name = $info['basename']; // copy to a tmp location - $tmp = sys_get_temp_dir().'/'.strtr(substr(base64_encode(hash('sha256', uniqid(mt_rand(), true), true)), 0, 7), '/', '_'); + $tmp = sys_get_temp_dir().'/'.strtr(substr(base64_encode(hash('xxh128', uniqid(mt_rand(), true), true)), 0, 7), '/', '_'); if (\array_key_exists('extension', $info)) { $tmp .= '.'.$info['extension']; } diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index 20da6a427ec65..844153745b2cb 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -134,7 +134,7 @@ public function setAutoLastModified(): static */ public function setAutoEtag(): static { - $this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true))); + $this->setEtag(base64_encode(hash_file('xxh128', $this->file->getPathname(), true))); return $this; } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/MockArraySessionStorage.php b/src/Symfony/Component/HttpFoundation/Session/Storage/MockArraySessionStorage.php index 49b5ee5878c7f..8ba28835d77ca 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/MockArraySessionStorage.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/MockArraySessionStorage.php @@ -169,7 +169,7 @@ public function getMetadataBag(): MetadataBag */ protected function generateId(): string { - return hash('sha256', uniqid('ss_mock_', true)); + return hash('xxh128', uniqid('ss_mock_', true)); } protected function loadSession(): void diff --git a/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php index d31ce75816cf2..2b2e2e28125c5 100644 --- a/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php @@ -27,7 +27,7 @@ protected function beforeDispatch(string $eventName, object $event): void { switch ($eventName) { case KernelEvents::REQUEST: - $event->getRequest()->attributes->set('_stopwatch_token', substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); + $event->getRequest()->attributes->set('_stopwatch_token', substr(hash('xxh128', uniqid(mt_rand(), true)), 0, 6)); $this->stopwatch->openSection(); break; case KernelEvents::VIEW: diff --git a/src/Symfony/Component/HttpKernel/Profiler/Profiler.php b/src/Symfony/Component/HttpKernel/Profiler/Profiler.php index b022ed979f5be..44fef547f49e0 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/Profiler.php +++ b/src/Symfony/Component/HttpKernel/Profiler/Profiler.php @@ -136,7 +136,7 @@ public function collect(Request $request, Response $response, \Throwable $except return null; } - $profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); + $profile = new Profile(substr(hash('xxh128', uniqid(mt_rand(), true)), 0, 6)); $profile->setTime(time()); $profile->setUrl($request->getUri()); $profile->setMethod($request->getMethod()); diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php b/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php index d22200f1d2721..49004ee3dfad0 100644 --- a/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php +++ b/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php @@ -75,7 +75,7 @@ public function testCompleteWriteProcessAddFiles() - + a trans_en_a @@ -93,7 +93,7 @@ public function testCompleteWriteProcessAddFiles() - + post.num_comments {count, plural, one {# comment} other {# comments}} @@ -171,7 +171,7 @@ public function testWriteAddFileServerError() - + a trans_en_a @@ -236,7 +236,7 @@ public function testWriteUpdateFileServerError() - + a trans_en_a @@ -308,7 +308,7 @@ public function testWriteUploadTranslationsServerError() - + a trans_fr_a @@ -326,7 +326,7 @@ public function testWriteUploadTranslationsServerError() - + a trans_en_a @@ -415,11 +415,11 @@ public function testCompleteWriteProcessUpdateFiles() - + a trans_en_a - + b trans_en_b @@ -489,7 +489,7 @@ public function testCompleteWriteProcessAddFileAndUploadTranslations(TranslatorB - + a trans_en_a @@ -575,7 +575,7 @@ public static function getResponsesForProcessAddFileAndUploadTranslations(): \Ge - + a trans_fr_a @@ -602,7 +602,7 @@ public static function getResponsesForProcessAddFileAndUploadTranslations(): \Ge - + a trans_en_gb_a diff --git a/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php b/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php index 40de212b62f26..d965e0ee1e306 100644 --- a/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php +++ b/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php @@ -797,11 +797,11 @@ public function writeProvider(): \Generator - + general.back - + general.cancel Cancel @@ -837,11 +837,11 @@ public function writeProvider(): \Generator - + general.back zurück - + general.cancel Abbrechen diff --git a/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php b/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php index 22f0227b9d52f..382bb678bee11 100644 --- a/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php +++ b/src/Symfony/Component/Translation/Dumper/XliffFileDumper.php @@ -93,7 +93,7 @@ private function dumpXliff1(string $defaultLocale, MessageCatalogue $messages, ? foreach ($messages->all($domain) as $source => $target) { $translation = $dom->createElement('trans-unit'); - $translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._')); + $translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._')); $translation->setAttribute('resname', $source); $s = $translation->appendChild($dom->createElement('source')); @@ -167,7 +167,7 @@ private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ? foreach ($messages->all($domain) as $source => $target) { $translation = $dom->createElement('unit'); - $translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._')); + $translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._')); if (\strlen($source) <= 80) { $translation->setAttribute('name', $source); diff --git a/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php index c753495f9ddd7..c8ecf1cf9ae86 100644 --- a/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php +++ b/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php @@ -93,11 +93,11 @@ public function testPullNewXlf12Messages() - + new.foo newFoo - + note NOTE @@ -114,7 +114,7 @@ public function testPullNewXlf12Messages() - + say_hello Welcome, {firstname}! @@ -131,11 +131,11 @@ public function testPullNewXlf12Messages() - + new.foo nouveauFoo - + note NOTE @@ -152,7 +152,7 @@ public function testPullNewXlf12Messages() - + say_hello Bonjour, {firstname}! @@ -199,13 +199,13 @@ public function testPullNewXlf20Messages() - + new.foo newFoo - + note NOTE @@ -219,13 +219,13 @@ public function testPullNewXlf20Messages() - + new.foo nouveauFoo - + note NOTE @@ -377,11 +377,11 @@ public function testPullForceMessages() - + note UPDATED NOTE - + new.foo newFoo @@ -398,11 +398,11 @@ public function testPullForceMessages() - + note NOTE MISE À JOUR - + new.foo nouveauFoo @@ -420,11 +420,11 @@ public function testPullForceMessages() - + foo.error Bad value - + bar.error Bar error @@ -441,11 +441,11 @@ public function testPullForceMessages() - + foo.error Valeur invalide - + bar.error Bar erreur @@ -500,11 +500,11 @@ public function testPullForceIntlIcuMessages() - + note UPDATED NOTE - + new.foo newFoo @@ -521,11 +521,11 @@ public function testPullForceIntlIcuMessages() - + note NOTE MISE À JOUR - + new.foo nouveauFoo @@ -576,11 +576,11 @@ public function testPullMessagesWithDefaultLocale() - + new.foo newFoo - + note NOTE @@ -597,11 +597,11 @@ public function testPullMessagesWithDefaultLocale() - + new.foo nouveauFoo - + note NOTE @@ -653,11 +653,11 @@ public function testPullMessagesMultipleDomains() - + new.foo newFoo - + note NOTE @@ -674,11 +674,11 @@ public function testPullMessagesMultipleDomains() - + new.foo newFoo - + note NOTE diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0+intl-icu.xlf b/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0+intl-icu.xlf index 6294f162fd7b0..f96a786d540e8 100644 --- a/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0+intl-icu.xlf +++ b/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0+intl-icu.xlf @@ -1,7 +1,7 @@ - + foo bar diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-clean.xlf b/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-clean.xlf index ccc5ef7a72bd0..6002848bdf72d 100644 --- a/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-clean.xlf +++ b/src/Symfony/Component/Translation/Tests/Fixtures/resources-2.0-clean.xlf @@ -1,25 +1,25 @@ - + foo bar - + key - + key.with.cdata & ]]> - + translation.key.that.is.longer.than.eighty.characters.should.not.have.name.attribute value diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/resources-clean.xlf b/src/Symfony/Component/Translation/Tests/Fixtures/resources-clean.xlf index 00c8a5c2416e8..df163b952d37d 100644 --- a/src/Symfony/Component/Translation/Tests/Fixtures/resources-clean.xlf +++ b/src/Symfony/Component/Translation/Tests/Fixtures/resources-clean.xlf @@ -5,18 +5,18 @@ - + foo bar baz - + key baz qux - + key.with.cdata & ]]> diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/resources-clean.xliff b/src/Symfony/Component/Translation/Tests/Fixtures/resources-clean.xliff index 00c8a5c2416e8..df163b952d37d 100644 --- a/src/Symfony/Component/Translation/Tests/Fixtures/resources-clean.xliff +++ b/src/Symfony/Component/Translation/Tests/Fixtures/resources-clean.xliff @@ -5,18 +5,18 @@ - + foo bar baz - + key baz qux - + key.with.cdata & ]]> diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/resources-notes-meta.xlf b/src/Symfony/Component/Translation/Tests/Fixtures/resources-notes-meta.xlf index 7d5bbd40f643f..ba8fcd813e165 100644 --- a/src/Symfony/Component/Translation/Tests/Fixtures/resources-notes-meta.xlf +++ b/src/Symfony/Component/Translation/Tests/Fixtures/resources-notes-meta.xlf @@ -1,7 +1,7 @@ - + new true @@ -12,7 +12,7 @@ bar - + x_content Fuzzy diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/resources-target-attributes.xlf b/src/Symfony/Component/Translation/Tests/Fixtures/resources-target-attributes.xlf index 700d28186e89e..e5f37cc6ee0e8 100644 --- a/src/Symfony/Component/Translation/Tests/Fixtures/resources-target-attributes.xlf +++ b/src/Symfony/Component/Translation/Tests/Fixtures/resources-target-attributes.xlf @@ -5,7 +5,7 @@ - + foo bar diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/resources-tool-info.xlf b/src/Symfony/Component/Translation/Tests/Fixtures/resources-tool-info.xlf index 1c2ae954e5fcc..fc6e2e9272e93 100644 --- a/src/Symfony/Component/Translation/Tests/Fixtures/resources-tool-info.xlf +++ b/src/Symfony/Component/Translation/Tests/Fixtures/resources-tool-info.xlf @@ -5,7 +5,7 @@ - + foo bar diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index 63037c574c0e7..0d8e24d325dc8 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -324,7 +324,7 @@ private function getFallbackContent(MessageCatalogue $catalogue): string private function getCatalogueCachePath(string $locale): string { - return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php'; + return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('xxh128', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php'; } /** From e9215e8d1add870f8f9a459e5387316d814b0417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 17 Dec 2023 20:45:24 +0100 Subject: [PATCH 053/395] Update .gitattributes Remove bin/update_mime_types.php from exports --- src/Symfony/Component/Mime/.gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Mime/.gitattributes b/src/Symfony/Component/Mime/.gitattributes index 84c7add058fb5..f9bcc05ac1f23 100644 --- a/src/Symfony/Component/Mime/.gitattributes +++ b/src/Symfony/Component/Mime/.gitattributes @@ -1,3 +1,4 @@ +/Resources/bin/update_mime_types.php export-ignore /Tests export-ignore /phpunit.xml.dist export-ignore /.gitattributes export-ignore From ff6b548797a0b214b597d6668652feec90254434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 17 Dec 2023 20:55:35 +0100 Subject: [PATCH 054/395] Update .gitattributes Remove code generation commands from exports --- src/Symfony/Component/Intl/.gitattributes | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Intl/.gitattributes b/src/Symfony/Component/Intl/.gitattributes index 76f27cd45836a..cb66a03789321 100644 --- a/src/Symfony/Component/Intl/.gitattributes +++ b/src/Symfony/Component/Intl/.gitattributes @@ -1,5 +1,9 @@ +/Resources/bin/autoload.php export-ignore +/Resources/bin/common.php export-ignore +/Resources/bin/compile export-ignore +/Resources/bin/update-data.php export-ignore +/Resources/emoji export-ignore /Tests export-ignore /phpunit.xml.dist export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/Resources/emoji export-ignore From 9bf8c77fadd49e5ed948603354b27aedee8ec390 Mon Sep 17 00:00:00 2001 From: Massimiliano Arione Date: Sun, 17 Dec 2023 21:09:20 +0100 Subject: [PATCH 055/395] [Validator] update Italian translation --- .../Validator/Resources/translations/validators.it.xlf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf index d9d9d06611d42..4781b986d3681 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf @@ -426,6 +426,10 @@ Using hidden overlay characters is not allowed. Non è consentito utilizzare caratteri sovrapposti nascosti. + + The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}. + L'estensione del file non è valida ({{ extension }}). Le estensioni consentite sono {{ extensions }}. + From b87cbe2ede038d9055362eb7647401050279b378 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 18 Dec 2023 09:38:37 +0100 Subject: [PATCH 056/395] add Bluesky to the UnsupportedSchemeException --- .../Notifier/Exception/UnsupportedSchemeException.php | 4 ++++ .../Tests/Exception/UnsupportedSchemeExceptionTest.php | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index f0ea7a49603e1..c296b41730776 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -28,6 +28,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Bandwidth\BandwidthTransportFactory::class, 'package' => 'symfony/bandwidth-notifier', ], + 'bluesky' => [ + 'class' => Bridge\Bluesky\BlueskyTransportFactory::class, + 'package' => 'symfony/bluesky-notifier', + ], 'brevo' => [ 'class' => Bridge\Brevo\BrevoTransportFactory::class, 'package' => 'symfony/brevo-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index 94a1291154231..c81fe985baba1 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -29,6 +29,7 @@ public static function setUpBeforeClass(): void Bridge\AllMySms\AllMySmsTransportFactory::class => false, Bridge\AmazonSns\AmazonSnsTransportFactory::class => false, Bridge\Bandwidth\BandwidthTransportFactory::class => false, + Bridge\Bluesky\BlueskyTransportFactory::class => false, Bridge\Brevo\BrevoTransportFactory::class => false, Bridge\Chatwork\ChatworkTransportFactory::class => false, Bridge\Clickatell\ClickatellTransportFactory::class => false, @@ -117,6 +118,7 @@ public static function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \ yield ['allmysms', 'symfony/all-my-sms-notifier']; yield ['sns', 'symfony/amazon-sns-notifier']; yield ['bandwidth', 'symfony/bandwidth-notifier']; + yield ['bluesky', 'symfony/bluesky-notifier']; yield ['brevo', 'symfony/brevo-notifier']; yield ['clickatell', 'symfony/clickatell-notifier']; yield ['clicksend', 'symfony/click-send-notifier']; From e2f21a604e25fd83db5c4a14199d1956de0a10c4 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 18 Dec 2023 15:39:48 +0100 Subject: [PATCH 057/395] fix merge --- .../Tests/DependencyInjection/FrameworkExtensionTestCase.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 4fa2ab34c2060..4bd3c481f860b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -40,6 +40,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -1693,7 +1694,7 @@ public function testCachePoolServices() ->replaceArgument(0, $expectedSeed) ->replaceArgument(1, 12), (new ChildDefinition('cache.adapter.redis')) - ->replaceArgument(0, new Reference('.cache_connection.U5HliuY')) + ->replaceArgument(0, new Reference('.cache_connection.'.(\count((new \ReflectionMethod(ContainerConfigurator::class, 'extension'))->getParameters()) > 2 ? 'U5HliuY' : 'kYdiLgf'))) ->replaceArgument(1, $expectedSeed) ->replaceArgument(2, 12), ], From e901313d0a23bb56dbb4b616d44f2b0db6350fd0 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 18 Dec 2023 15:46:38 +0100 Subject: [PATCH 058/395] fix merge --- .../DependencyInjection/CompleteConfigurationTestCase.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php index 858f99e748635..cedfb18d2b886 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php @@ -18,6 +18,7 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpFoundation\RequestMatcher\AttributesRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher; @@ -137,7 +138,7 @@ public function testFirewalls() [ 'simple', 'security.user_checker', - '.security.request_matcher.rud_2nr', + \count((new \ReflectionMethod(ContainerConfigurator::class, 'extension'))->getParameters()) > 2 ? '.security.request_matcher.rud_2nr' : '.security.request_matcher.h5ibf38', false, false, '', @@ -187,7 +188,7 @@ public function testFirewalls() [ 'host', 'security.user_checker', - '.security.request_matcher.ap9sh8g', + \count((new \ReflectionMethod(ContainerConfigurator::class, 'extension'))->getParameters()) > 2 ? '.security.request_matcher.ap9sh8g' : '.security.request_matcher.bcmu4fb', true, false, 'security.user.provider.concrete.default', From 578eb296ce98edb2896a70c8e32382f77b5937c1 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 18 Dec 2023 11:11:27 +0100 Subject: [PATCH 059/395] [Cache] Fix failing test --- .../CouchbaseCollectionAdapterTest.php | 27 +++++++------------ src/Symfony/Component/Cache/composer.json | 1 + 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php index 4260cee980a08..2f16bb88454e7 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Cache\Tests\Adapter; -use Couchbase\Collection; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter; @@ -21,41 +21,32 @@ * @requires extension couchbase >=3.0.0 * * @group integration + * @group legacy * * @author Antonio Jose Cerezo Aranda */ class CouchbaseCollectionAdapterTest extends AdapterTestCase { + use ExpectDeprecationTrait; + protected $skippedTests = [ 'testClearPrefix' => 'Couchbase cannot clear by prefix', ]; - protected static Collection $client; - - public static function setupBeforeClass(): void + public static function setUpBeforeClass(): void { if (!CouchbaseCollectionAdapter::isSupported()) { self::markTestSkipped('Couchbase >= 3.0.0 < 4.0.0 is required.'); } - - self::$client = AbstractAdapter::createConnection('couchbase://'.getenv('COUCHBASE_HOST').'/cache', - ['username' => getenv('COUCHBASE_USER'), 'password' => getenv('COUCHBASE_PASS')] - ); } public function createCachePool($defaultLifetime = 0): CacheItemPoolInterface { - if (!CouchbaseCollectionAdapter::isSupported()) { - self::markTestSkipped('Couchbase >= 3.0.0 < 4.0.0 is required.'); - } + $this->expectDeprecation('Since symfony/cache 7.1: The "Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter" class is deprecated, use "Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter" instead.'); - $client = $defaultLifetime - ? AbstractAdapter::createConnection('couchbase://' - .getenv('COUCHBASE_USER') - .':'.getenv('COUCHBASE_PASS') - .'@'.getenv('COUCHBASE_HOST') - .'/cache') - : self::$client; + $client = AbstractAdapter::createConnection('couchbase://'.getenv('COUCHBASE_HOST').'/cache', + ['username' => getenv('COUCHBASE_USER'), 'password' => getenv('COUCHBASE_PASS')] + ); return new CouchbaseCollectionAdapter($client, str_replace('\\', '.', __CLASS__), $defaultLifetime); } diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json index d91297eb0d252..d537037ae6d09 100644 --- a/src/Symfony/Component/Cache/composer.json +++ b/src/Symfony/Component/Cache/composer.json @@ -25,6 +25,7 @@ "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^2.5|^3", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/service-contracts": "^2.5|^3", "symfony/var-exporter": "^6.4|^7.0" }, From ae1774fed4ac066763c612e56572567a621f9fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Mon, 18 Dec 2023 16:11:11 +0100 Subject: [PATCH 060/395] [Intl][Tests] Use static data providers --- .../Intl/Tests/ResourceBundleTestCase.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundleTestCase.php b/src/Symfony/Component/Intl/Tests/ResourceBundleTestCase.php index e2a67c8469d68..22225caccd860 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundleTestCase.php +++ b/src/Symfony/Component/Intl/Tests/ResourceBundleTestCase.php @@ -758,46 +758,46 @@ protected function tearDown(): void \Locale::setDefault($this->defaultLocale); } - public function provideLocales() + public static function provideLocales() { return array_map( fn ($locale) => [$locale], - $this->getLocales() + self::getLocales() ); } - public function provideLocaleAliases() + public static function provideLocaleAliases() { return array_map( fn ($alias, $ofLocale) => [$alias, $ofLocale], - array_keys($this->getLocaleAliases()), - $this->getLocaleAliases() + array_keys(self::getLocaleAliases()), + self::getLocaleAliases() ); } - public function provideRootLocales() + public static function provideRootLocales() { return array_map( fn ($locale) => [$locale], - $this->getRootLocales() + self::getRootLocales() ); } - protected function getLocales() + protected static function getLocales() { return self::LOCALES; } - protected function getLocaleAliases() + protected static function getLocaleAliases() { return self::LOCALE_ALIASES; } - protected function getRootLocales() + protected static function getRootLocales() { if (null === self::$rootLocales) { - self::$rootLocales = array_filter($this->getLocales(), fn ($locale) => // no locales for which fallback is possible (e.g "en_GB") -!str_contains($locale, '_')); + // ignore locales for which fallback is possible (e.g "en_GB") + self::$rootLocales = array_filter(self::getLocales(), fn ($locale) => !str_contains($locale, '_')); } return self::$rootLocales; From e29033486d39e5f0c7778d3ee0e56ceda2074743 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 18 Dec 2023 08:46:12 +0100 Subject: [PATCH 061/395] Code updates --- .github/get-modified-packages.php | 34 +++++-------------- .../DataCollector/DoctrineDataCollector.php | 3 +- .../Bridge/Twig/Command/DebugCommand.php | 6 +--- .../Console/Descriptor/JsonDescriptor.php | 2 +- .../Console/Descriptor/XmlDescriptor.php | 2 +- .../SecurityBundle/Security/FirewallMap.php | 8 +---- .../Component/Console/Helper/Table.php | 4 +-- src/Symfony/Component/Dotenv/Dotenv.php | 2 +- src/Symfony/Component/Filesystem/Path.php | 4 +-- .../Form/Console/Descriptor/Descriptor.php | 2 +- .../Form/Extension/Core/Type/ChoiceType.php | 2 +- .../Form/Util/OrderedHashMapIterator.php | 4 --- .../Component/HttpClient/CurlHttpClient.php | 2 +- .../Component/HttpFoundation/Cookie.php | 2 +- .../Bridge/AmazonSqs/Transport/Connection.php | 4 +-- .../DependencyInjection/MessengerPass.php | 2 +- .../Component/Mime/CharacterStream.php | 2 +- .../Notifier/Bridge/Ntfy/NtfyOptions.php | 16 ++++----- .../PropertyAccess/PropertyAccessor.php | 8 ++--- .../Core/Authorization/Voter/RoleVoter.php | 6 ++-- .../Http/Firewall/ContextListener.php | 2 +- .../Component/Security/Http/HttpUtils.php | 4 +-- .../Session/SessionAuthenticationStrategy.php | 5 +-- .../Normalizer/DataUriNormalizer.php | 2 +- .../Validator/Constraints/UniqueValidator.php | 6 +--- .../VarDumper/Dumper/AbstractDumper.php | 2 +- src/Symfony/Component/Yaml/Dumper.php | 2 +- src/Symfony/Component/Yaml/Escaper.php | 2 +- 28 files changed, 45 insertions(+), 95 deletions(-) diff --git a/.github/get-modified-packages.php b/.github/get-modified-packages.php index 990aa35f038f6..11478cbe935c0 100644 --- a/.github/get-modified-packages.php +++ b/.github/get-modified-packages.php @@ -19,31 +19,15 @@ function getPackageType(string $packageDir): string { - if (preg_match('@Symfony/Bridge/@', $packageDir)) { - return 'bridge'; - } - - if (preg_match('@Symfony/Bundle/@', $packageDir)) { - return 'bundle'; - } - - if (preg_match('@Symfony/Component/[^/]+/Bridge/@', $packageDir)) { - return 'component_bridge'; - } - - if (preg_match('@Symfony/Component/@', $packageDir)) { - return 'component'; - } - - if (preg_match('@Symfony/Contracts/@', $packageDir)) { - return 'contract'; - } - - if (preg_match('@Symfony/Contracts$@', $packageDir)) { - return 'contracts'; - } - - throw new \LogicException(); + return match (true) { + str_contains($packageDir, 'Symfony/Bridge/') => 'bridge', + str_contains($packageDir, 'Symfony/Bundle/') => 'bundle', + preg_match('@Symfony/Component/[^/]+/Bridge/@', $packageDir) => 'component_bridge', + str_contains($packageDir, 'Symfony/Component/') => 'component', + str_contains($packageDir, 'Symfony/Contracts/') => 'contract', + str_ends_with($packageDir, 'Symfony/Contracts') => 'contracts', + default => throw new \LogicException(), + }; } $newPackage = []; diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php index 92ce82e479641..6d6146ec50ac3 100644 --- a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php +++ b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php @@ -163,8 +163,7 @@ private function sanitizeQuery(string $connectionName, array $query): array $query['types'][$j] = $type->getBindingType(); try { $param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform()); - } catch (\TypeError $e) { - } catch (ConversionException $e) { + } catch (\TypeError|ConversionException) { } } } diff --git a/src/Symfony/Bridge/Twig/Command/DebugCommand.php b/src/Symfony/Bridge/Twig/Command/DebugCommand.php index b18100cb7df9f..1e1c446dbdbf3 100644 --- a/src/Symfony/Bridge/Twig/Command/DebugCommand.php +++ b/src/Symfony/Bridge/Twig/Command/DebugCommand.php @@ -587,11 +587,7 @@ private function getFilesystemLoaders(): array private function getFileLink(string $absolutePath): string { - if (null === $this->fileLinkFormatter) { - return ''; - } - - return (string) $this->fileLinkFormatter->format($absolutePath, 1); + return (string) $this->fileLinkFormatter?->format($absolutePath, 1); } private function getAvailableFormatOptions(): array diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php index 35c86c58477b6..f3c70310eee13 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php @@ -323,7 +323,7 @@ private function getContainerAliasData(Alias $alias): array private function getEventDispatcherListenersData(EventDispatcherInterface $eventDispatcher, array $options): array { $data = []; - $event = \array_key_exists('event', $options) ? $options['event'] : null; + $event = $options['event'] ?? null; if (null !== $event) { foreach ($eventDispatcher->getListeners($event) as $listener) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php index 069dcf09f64fc..d530936d704db 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php @@ -501,7 +501,7 @@ private function getContainerParameterDocument(mixed $parameter, ?array $depreca private function getEventDispatcherListenersDocument(EventDispatcherInterface $eventDispatcher, array $options): \DOMDocument { - $event = \array_key_exists('event', $options) ? $options['event'] : null; + $event = $options['event'] ?? null; $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($eventDispatcherXML = $dom->createElement('event-dispatcher')); diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php index 6f1bdfcdd4892..fbb44caeded62 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php @@ -46,13 +46,7 @@ public function getListeners(Request $request): array public function getFirewallConfig(Request $request): ?FirewallConfig { - $context = $this->getFirewallContext($request); - - if (null === $context) { - return null; - } - - return $context->getConfig(); + return $this->getFirewallContext($request)?->getConfig(); } private function getFirewallContext(Request $request): ?FirewallContext diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index fe2ac87c1c784..fd2e94d5dbe7d 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -717,7 +717,7 @@ private function fillNextRows(array $rows, int $line): array foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { // we need to know if $unmergedRow will be merged or inserted into $rows - if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) { + if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRow) <= $this->numberOfColumns)) { foreach ($unmergedRow as $cellKey => $cell) { // insert cell into row at cellKey position array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]); @@ -726,7 +726,7 @@ private function fillNextRows(array $rows, int $line): array $row = $this->copyRow($rows, $unmergedRowKey - 1); foreach ($unmergedRow as $column => $cell) { if (!empty($cell)) { - $row[$column] = $unmergedRow[$column]; + $row[$column] = $cell; } } array_splice($rows, $unmergedRowKey, 0, [$row]); diff --git a/src/Symfony/Component/Dotenv/Dotenv.php b/src/Symfony/Component/Dotenv/Dotenv.php index 6e693ac28b329..5fb8fddc48dcc 100644 --- a/src/Symfony/Component/Dotenv/Dotenv.php +++ b/src/Symfony/Component/Dotenv/Dotenv.php @@ -463,7 +463,7 @@ private function resolveCommands(string $value, array $loadedVars): string throw $this->createFormatException(sprintf('Issue expanding a command (%s)', $process->getErrorOutput())); } - return preg_replace('/[\r\n]+$/', '', $process->getOutput()); + return rtrim($process->getOutput(), "\n\r"); }, $value); } diff --git a/src/Symfony/Component/Filesystem/Path.php b/src/Symfony/Component/Filesystem/Path.php index 6643962351feb..3aa1910ec0d19 100644 --- a/src/Symfony/Component/Filesystem/Path.php +++ b/src/Symfony/Component/Filesystem/Path.php @@ -346,13 +346,13 @@ public static function changeExtension(string $path, string $extension): string $extension = ltrim($extension, '.'); // No extension for paths - if ('/' === substr($path, -1)) { + if (str_ends_with($path, '/')) { return $path; } // No actual extension in path if (empty($actualExtension)) { - return $path.('.' === substr($path, -1) ? '' : '.').$extension; + return $path.(str_ends_with($path, '.') ? '' : '.').$extension; } return substr($path, 0, -\strlen($actualExtension)).$extension; diff --git a/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php b/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php index b8d0399ee172c..f4835fb1eeb00 100644 --- a/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php +++ b/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php @@ -128,7 +128,7 @@ protected function getOptionDefinition(OptionsResolver $optionsResolver, string } } - if (isset($definition['deprecation']) && isset($definition['deprecation']['message']) && \is_string($definition['deprecation']['message'])) { + if (isset($definition['deprecation']['message']) && \is_string($definition['deprecation']['message'])) { $definition['deprecationMessage'] = strtr($definition['deprecation']['message'], ['%name%' => $option]); $definition['deprecationPackage'] = $definition['deprecation']['package']; $definition['deprecationVersion'] = $definition['deprecation']['version']; diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 0030a835ff636..8cdf89e511f0a 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -437,7 +437,7 @@ private function createChoiceList(array $options): ChoiceListInterface } // Harden against NULL values (like in EntityType and ModelType) - $choices = null !== $options['choices'] ? $options['choices'] : []; + $choices = $options['choices'] ?? []; return $this->choiceListFactory->createListFromChoices( $choices, diff --git a/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php b/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php index a0c400e21a58b..4a8eebe61d921 100644 --- a/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php +++ b/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php @@ -98,10 +98,6 @@ public function next(): void public function key(): mixed { - if (null === $this->key) { - return null; - } - return $this->key; } diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index bbaa4de28893c..acd492d04439d 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -106,7 +106,7 @@ public function request(string $method, string $url, array $options = []): Respo \CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS, \CURLOPT_REDIR_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS, \CURLOPT_FOLLOWLOCATION => true, - \CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0, + \CURLOPT_MAXREDIRS => max(0, $options['max_redirects']), \CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects \CURLOPT_TIMEOUT => 0, \CURLOPT_PROXY => $proxy, diff --git a/src/Symfony/Component/HttpFoundation/Cookie.php b/src/Symfony/Component/HttpFoundation/Cookie.php index 709f484eddc6d..b8982f75fc8d2 100644 --- a/src/Symfony/Component/HttpFoundation/Cookie.php +++ b/src/Symfony/Component/HttpFoundation/Cookie.php @@ -340,7 +340,7 @@ public function getMaxAge(): int { $maxAge = $this->expire - time(); - return 0 >= $maxAge ? 0 : $maxAge; + return max(0, $maxAge); } /** diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/Connection.php index c61b79a16cb1c..f01f7bd8a1332 100644 --- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/Connection.php @@ -358,8 +358,8 @@ public function send(string $body, array $headers, int $delay = 0, string $messa } if (self::isFifoQueue($this->configuration['queue_name'])) { - $parameters['MessageGroupId'] = null !== $messageGroupId ? $messageGroupId : __METHOD__; - $parameters['MessageDeduplicationId'] = null !== $messageDeduplicationId ? $messageDeduplicationId : sha1(json_encode(['body' => $body, 'headers' => $headers])); + $parameters['MessageGroupId'] = $messageGroupId ?? __METHOD__; + $parameters['MessageDeduplicationId'] = $messageDeduplicationId ?? sha1(json_encode(['body' => $body, 'headers' => $headers])); unset($parameters['DelaySeconds']); } diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index b2f9587d3e506..008e04b339e6d 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -138,7 +138,7 @@ private function registerHandlers(ContainerBuilder $container, array $busIds): v } if (null === $message) { - throw new RuntimeException(sprintf('Invalid handler service "%s": the list of messages to handle is empty.', $serviceId, $r->getName())); + throw new RuntimeException(sprintf('Invalid handler service "%s": the list of messages to handle is empty.', $serviceId)); } } } diff --git a/src/Symfony/Component/Mime/CharacterStream.php b/src/Symfony/Component/Mime/CharacterStream.php index 21d7bc5f01737..572cf820bed01 100644 --- a/src/Symfony/Component/Mime/CharacterStream.php +++ b/src/Symfony/Component/Mime/CharacterStream.php @@ -111,7 +111,7 @@ public function read(int $length): ?string $this->currentPos += $length; } else { $end = $this->currentPos + $length; - $end = $end > $this->charCount ? $this->charCount : $end; + $end = min($end, $this->charCount); $ret = ''; $start = 0; if ($this->currentPos > 0) { diff --git a/src/Symfony/Component/Notifier/Bridge/Ntfy/NtfyOptions.php b/src/Symfony/Component/Notifier/Bridge/Ntfy/NtfyOptions.php index 03a7dba3400ae..6b33df67558ee 100644 --- a/src/Symfony/Component/Notifier/Bridge/Ntfy/NtfyOptions.php +++ b/src/Symfony/Component/Notifier/Bridge/Ntfy/NtfyOptions.php @@ -67,16 +67,12 @@ public function setTitle(string $title): self public function setStringPriority(string $priority): self { - switch ($priority) { - case Notification::IMPORTANCE_URGENT: - return $this->setPriority(self::PRIORITY_URGENT); - case Notification::IMPORTANCE_HIGH: - return $this->setPriority(self::PRIORITY_HIGH); - case Notification::IMPORTANCE_LOW: - return $this->setPriority(self::PRIORITY_LOW); - default: - return $this->setPriority(self::PRIORITY_DEFAULT); - } + return match ($priority) { + Notification::IMPORTANCE_URGENT => $this->setPriority(self::PRIORITY_URGENT), + Notification::IMPORTANCE_HIGH => $this->setPriority(self::PRIORITY_HIGH), + Notification::IMPORTANCE_LOW => $this->setPriority(self::PRIORITY_LOW), + default => $this->setPriority(self::PRIORITY_DEFAULT), + }; } public function setPriority(int $priority): self diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 8393a332459a0..36b05040693dc 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -214,9 +214,7 @@ public function isReadable(object|array $objectOrArray, string|PropertyPathInter $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); return true; - } catch (AccessException) { - return false; - } catch (UnexpectedTypeException) { + } catch (AccessException|UnexpectedTypeException) { return false; } } @@ -249,9 +247,7 @@ public function isWritable(object|array $objectOrArray, string|PropertyPathInter } return true; - } catch (AccessException) { - return false; - } catch (UnexpectedTypeException) { + } catch (AccessException|UnexpectedTypeException) { return false; } } diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php index dbf5047817ca7..76de3a32c7f3a 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php @@ -38,10 +38,8 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes): } $result = VoterInterface::ACCESS_DENIED; - foreach ($roles as $role) { - if ($attribute === $role) { - return VoterInterface::ACCESS_GRANTED; - } + if (\in_array($attribute, $roles, true)) { + return VoterInterface::ACCESS_GRANTED; } } diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index 3de1aab1558cd..7df6899ee9b81 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -283,7 +283,7 @@ private static function hasUserChanged(UserInterface $originalUser, TokenInterfa $refreshedUser = $refreshedToken->getUser(); if ($originalUser instanceof EquatableInterface) { - return !(bool) $originalUser->isEqualTo($refreshedUser); + return !$originalUser->isEqualTo($refreshedUser); } if ($originalUser instanceof PasswordAuthenticatedUserInterface || $refreshedUser instanceof PasswordAuthenticatedUserInterface) { diff --git a/src/Symfony/Component/Security/Http/HttpUtils.php b/src/Symfony/Component/Security/Http/HttpUtils.php index 783430c0fc48c..ce69027c86d58 100644 --- a/src/Symfony/Component/Security/Http/HttpUtils.php +++ b/src/Symfony/Component/Security/Http/HttpUtils.php @@ -121,9 +121,7 @@ public function checkRequestPath(Request $request, string $path): bool } return isset($parameters['_route']) && $path === $parameters['_route']; - } catch (MethodNotAllowedException) { - return false; - } catch (ResourceNotFoundException) { + } catch (MethodNotAllowedException|ResourceNotFoundException) { return false; } } diff --git a/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php index 79d75d84cd213..7f04d6ce3531c 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php @@ -51,10 +51,7 @@ public function onAuthentication(Request $request, TokenInterface $token): void case self::MIGRATE: $request->getSession()->migrate(true); - - if ($this->csrfTokenStorage) { - $this->csrfTokenStorage->clear(); - } + $this->csrfTokenStorage?->clear(); return; diff --git a/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php index c1aa9695b2c2f..3e0a6124179b7 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php @@ -89,7 +89,7 @@ public function supportsNormalization(mixed $data, string $format = null, array */ public function denormalize(mixed $data, string $type, string $format = null, array $context = []): \SplFileInfo { - if (null === $data || !preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i', $data)) { + if (null === $data || !preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9\!\$\&\\\'\,\(\)\*\+\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i', $data)) { throw NotNormalizableValueException::createForUnexpectedDataType('The provided "data:" URI is not valid.', $data, ['string'], $context['deserialization_path'] ?? null, true); } diff --git a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php index f4e4012c642d3..b3782aac08064 100644 --- a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php @@ -60,11 +60,7 @@ public function validate(mixed $value, Constraint $constraint): void private function getNormalizer(Unique $unique): callable { - if (null === $unique->normalizer) { - return static fn ($value) => $value; - } - - return $unique->normalizer; + return $unique->normalizer ?? static fn ($value) => $value; } private function reduceElementKeys(array $fields, array $element): array diff --git a/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php b/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php index 79d97666ca865..99c78f2806bfa 100644 --- a/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php @@ -90,7 +90,7 @@ public function setCharset(string $charset): string $prev = $this->charset; $charset = strtoupper($charset); - $charset = null === $charset || 'UTF-8' === $charset || 'UTF8' === $charset ? 'CP1252' : $charset; + $charset = 'UTF-8' === $charset || 'UTF8' === $charset ? 'CP1252' : $charset; $this->charset = $charset; diff --git a/src/Symfony/Component/Yaml/Dumper.php b/src/Symfony/Component/Yaml/Dumper.php index 04646c5cdd337..226a81c9feb5a 100644 --- a/src/Symfony/Component/Yaml/Dumper.php +++ b/src/Symfony/Component/Yaml/Dumper.php @@ -169,7 +169,7 @@ private function getBlockIndentationIndicator(string $value): string // http://www.yaml.org/spec/1.2/spec.html#id2793979 foreach ($lines as $line) { if ('' !== trim($line, ' ')) { - return (' ' === substr($line, 0, 1)) ? (string) $this->indentation : ''; + return str_starts_with($line, ' ') ? (string) $this->indentation : ''; } } diff --git a/src/Symfony/Component/Yaml/Escaper.php b/src/Symfony/Component/Yaml/Escaper.php index e8090d8c63b86..044f1a3b1d00a 100644 --- a/src/Symfony/Component/Yaml/Escaper.php +++ b/src/Symfony/Component/Yaml/Escaper.php @@ -80,7 +80,7 @@ public static function requiresSingleQuoting(string $value): bool // Determines if the PHP value contains any single characters that would // cause it to require single quoting in YAML. - return 0 < preg_match('/[ \s \' " \: \{ \} \[ \] , & \* \# \?] | \A[ \- ? | < > = ! % @ ` \p{Zs}]/xu', $value); + return 0 < preg_match('/[\s\'"\:\{\}\[\],&\*\#\?] | \A[\-?|<>=!%@`\p{Zs}]/xu', $value); } /** From 857db8f0af17b064241452265ef9f3b959e1c89a Mon Sep 17 00:00:00 2001 From: Shamimul Alam Date: Mon, 18 Dec 2023 16:15:31 +0600 Subject: [PATCH 062/395] [VarDumper] Added default message for dd function --- .../Component/VarDumper/Resources/functions/dump.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Symfony/Component/VarDumper/Resources/functions/dump.php b/src/Symfony/Component/VarDumper/Resources/functions/dump.php index f2ff74c0caee6..e6ade0dfaed38 100644 --- a/src/Symfony/Component/VarDumper/Resources/functions/dump.php +++ b/src/Symfony/Component/VarDumper/Resources/functions/dump.php @@ -49,6 +49,12 @@ function dd(mixed ...$vars): never header('HTTP/1.1 500 Internal Server Error'); } + if (!$vars) { + VarDumper::dump(new ScalarStub('🐛')); + + exit(1); + } + if (array_key_exists(0, $vars) && 1 === count($vars)) { VarDumper::dump($vars[0]); } else { From c1a653e9d9a4b9e193ebc76bbc02a097c0a62e21 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 19 Dec 2023 11:15:50 +0100 Subject: [PATCH 063/395] [Cache] Fix failing test --- .../Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php | 2 +- .../Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php index e51d391f970a5..d7511bc67f45c 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php @@ -20,7 +20,7 @@ * @requires extension couchbase <3.0.0 * @requires extension couchbase >=2.6.0 * - * @group integration legacy + * @group legacy integration * * @author Antonio Jose Cerezo Aranda */ diff --git a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php index 2f16bb88454e7..f111c609e7ab9 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php @@ -20,8 +20,7 @@ * @requires extension couchbase <4.0.0 * @requires extension couchbase >=3.0.0 * - * @group integration - * @group legacy + * @group legacy integration * * @author Antonio Jose Cerezo Aranda */ From 5ab4068565778ffd92337c2159cafbf75874de06 Mon Sep 17 00:00:00 2001 From: Florian Hermann Date: Fri, 8 Dec 2023 21:21:17 +0100 Subject: [PATCH 064/395] [Validator] Add `list` and `associative_array` types to `Type` constraint --- src/Symfony/Component/Validator/CHANGELOG.md | 5 +++++ .../Component/Validator/Constraints/TypeValidator.php | 4 ++++ .../Validator/Tests/Constraints/TypeValidatorTest.php | 10 ++++++++++ 3 files changed, 19 insertions(+) diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 73c4f27715ef5..c2c41d6daa4a6 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `list` and `associative_array` types to `Type` constraint + 7.0 --- diff --git a/src/Symfony/Component/Validator/Constraints/TypeValidator.php b/src/Symfony/Component/Validator/Constraints/TypeValidator.php index 026db36ebc91a..94c0c8639d46b 100644 --- a/src/Symfony/Component/Validator/Constraints/TypeValidator.php +++ b/src/Symfony/Component/Validator/Constraints/TypeValidator.php @@ -36,6 +36,8 @@ class TypeValidator extends ConstraintValidator 'string' => 'is_string', 'scalar' => 'is_scalar', 'array' => 'is_array', + 'list' => 'is_array && array_is_list', + 'associative_array' => 'is_array && !array_is_list', 'iterable' => 'is_iterable', 'countable' => 'is_countable', 'callable' => 'is_callable', @@ -73,6 +75,8 @@ public function validate(mixed $value, Constraint $constraint): void 'finite-float' => \is_float($value) && is_finite($value), 'finite-number' => \is_int($value) || \is_float($value) && is_finite($value), 'number' => \is_int($value) || \is_float($value) && !is_nan($value), + 'list' => \is_array($value) && array_is_list($value), + 'associative_array' => \is_array($value) && !array_is_list($value), default => self::VALIDATION_FUNCTIONS[$type]($value), }) { return; diff --git a/src/Symfony/Component/Validator/Tests/Constraints/TypeValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/TypeValidatorTest.php index 8b4fe25314b11..99714407fad1b 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/TypeValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/TypeValidatorTest.php @@ -97,6 +97,10 @@ public static function getValidValues() [1.5, 'finite-number'], ['12345', 'string'], [[], 'array'], + [[], 'list'], + [[1, 2, 3], 'list'], + [['abc' => 1], 'associative_array'], + [[1 => 1], 'associative_array'], [$object, 'object'], [$object, 'stdClass'], [$file, 'resource'], @@ -166,6 +170,12 @@ public static function getInvalidValues() [$file, 'float', 'resource'], [$file, 'string', 'resource'], [$file, 'object', 'resource'], + [[1 => 1], 'list', 'array'], + [['abc' => 1], 'list', 'array'], + ['abcd1', 'list', '"abcd1"'], + [[], 'associative_array', 'array'], + [[1, 2, 3], 'associative_array', 'array'], + ['abcd1', 'associative_array', '"abcd1"'], ['12a34', 'digit', '"12a34"'], ['1a#23', 'alnum', '"1a#23"'], ['abcd1', 'alpha', '"abcd1"'], From 93f18121c0b0d526bf6e7770ff9e9f0cc9dc25a5 Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Sat, 9 Dec 2023 12:12:25 +0100 Subject: [PATCH 065/395] [Uid] Add `UuidV6::fromV1()` and `UuidV7::fromV1()` methods --- src/Symfony/Component/Uid/BinaryUtil.php | 16 +++++++++-- src/Symfony/Component/Uid/CHANGELOG.md | 5 ++++ src/Symfony/Component/Uid/Tests/UuidTest.php | 29 ++++++++++++++++++++ src/Symfony/Component/Uid/UuidV6.php | 7 +++++ src/Symfony/Component/Uid/UuidV7.php | 22 +++++++++++++++ 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Uid/BinaryUtil.php b/src/Symfony/Component/Uid/BinaryUtil.php index 8fd19d8674af0..fd158345546d3 100644 --- a/src/Symfony/Component/Uid/BinaryUtil.php +++ b/src/Symfony/Component/Uid/BinaryUtil.php @@ -118,8 +118,10 @@ public static function add(string $a, string $b): string /** * @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal + * + * @return string Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 as a numeric string */ - public static function hexToDateTime(string $time): \DateTimeImmutable + public static function hexToNumericString(string $time): string { if (\PHP_INT_SIZE >= 8) { $time = (string) (hexdec($time) - self::TIME_OFFSET_INT); @@ -140,7 +142,17 @@ public static function hexToDateTime(string $time): \DateTimeImmutable $time = '-' === $time[0] ? '-'.str_pad(substr($time, 1), 8, '0', \STR_PAD_LEFT) : str_pad($time, 8, '0', \STR_PAD_LEFT); } - return \DateTimeImmutable::createFromFormat('U.u?', substr_replace($time, '.', -7, 0)); + return $time; + } + + /** + * Sub-microseconds are lost since they are not handled by \DateTimeImmutable. + * + * @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal + */ + public static function hexToDateTime(string $time): \DateTimeImmutable + { + return \DateTimeImmutable::createFromFormat('U.u?', substr_replace(self::hexToNumericString($time), '.', -7, 0)); } /** diff --git a/src/Symfony/Component/Uid/CHANGELOG.md b/src/Symfony/Component/Uid/CHANGELOG.md index b82133751c0a9..1e07caa42a721 100644 --- a/src/Symfony/Component/Uid/CHANGELOG.md +++ b/src/Symfony/Component/Uid/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `UuidV6::fromV1()` and `UuidV7::fromV1()` + 6.2 --- diff --git a/src/Symfony/Component/Uid/Tests/UuidTest.php b/src/Symfony/Component/Uid/Tests/UuidTest.php index 5e05b89f6e395..172bc18340950 100644 --- a/src/Symfony/Component/Uid/Tests/UuidTest.php +++ b/src/Symfony/Component/Uid/Tests/UuidTest.php @@ -427,4 +427,33 @@ public function testFromStringBase58Padding() { $this->assertInstanceOf(Uuid::class, Uuid::fromString('111111111u9QRyVM94rdmZ')); } + + public function testV6FromV1() + { + $uuidV1 = new UuidV1('8189d3de-9670-11ee-b9d1-0242ac120002'); + $uuidV6 = UuidV6::fromV1($uuidV1); + + $this->assertEquals($uuidV1->getDateTime(), $uuidV6->getDateTime()); + $this->assertSame($uuidV1->getNode(), $uuidV6->getNode()); + $this->assertEquals($uuidV6, UuidV6::fromV1($uuidV1)); + } + + public function testV7FromV1BeforeUnixEpochThrows() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('UUIDv1 with a timestamp before Unix epoch cannot be converted to UUIDv7'); + + UuidV7::fromV1(new UuidV1('9aba8000-ff00-11b0-b3db-3b3fc83afdfc')); // Timestamp is 1969-01-01 00:00:00.0000000 + } + + public function testV7FromV1() + { + $uuidV1 = new UuidV1('eb248d80-ea4f-11ec-9d2a-839425e6fb88'); + $sameUuidV1100NanosecondsLater = new UuidV1('eb248d81-ea4f-11ec-9d2a-839425e6fb88'); + $uuidV7 = UuidV7::fromV1($uuidV1); + + $this->assertEquals($uuidV1->getDateTime(), $uuidV7->getDateTime()); + $this->assertEquals($uuidV7, UuidV7::fromV1($uuidV1)); + $this->assertNotEquals($uuidV7, UuidV7::fromV1($sameUuidV1100NanosecondsLater)); + } } diff --git a/src/Symfony/Component/Uid/UuidV6.php b/src/Symfony/Component/Uid/UuidV6.php index 1cecf408d9177..bed75bcb58135 100644 --- a/src/Symfony/Component/Uid/UuidV6.php +++ b/src/Symfony/Component/Uid/UuidV6.php @@ -43,6 +43,13 @@ public function getNode(): string return substr($this->uid, 24); } + public static function fromV1(UuidV1 $uuidV1): self + { + $uuidV1 = $uuidV1->toRfc4122(); + + return new self(substr($uuidV1, 15, 3).substr($uuidV1, 9, 4).$uuidV1[0].'-'.substr($uuidV1, 1, 4).'-6'.substr($uuidV1, 5, 3).substr($uuidV1, 18, 6).substr($uuidV1, 24)); + } + public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string { $uuidV1 = UuidV1::generate($time, $node); diff --git a/src/Symfony/Component/Uid/UuidV7.php b/src/Symfony/Component/Uid/UuidV7.php index 88797d37eda67..ce90b2a3da111 100644 --- a/src/Symfony/Component/Uid/UuidV7.php +++ b/src/Symfony/Component/Uid/UuidV7.php @@ -49,6 +49,28 @@ public function getDateTime(): \DateTimeImmutable return \DateTimeImmutable::createFromFormat('U.v', substr_replace($time, '.', -3, 0)); } + /** + * Sub-millisecond timestamp precision is lost since UUIDv7 don't support it. + */ + public static function fromV1(UuidV1 $uuidV1): self + { + $uuidV1 = $uuidV1->toRfc4122(); + $time = '0'.substr($uuidV1, 15, 3).substr($uuidV1, 9, 4).substr($uuidV1, 0, 8); + $time = BinaryUtil::hexToNumericString($time); + if ('-' === $time[0]) { + throw new \InvalidArgumentException('UUIDv1 with a timestamp before Unix epoch cannot be converted to UUIDv7'); + } + + $time = str_pad($time, 5, '0', \STR_PAD_LEFT); + + return new self(substr_replace(sprintf( + '%012s-7%03s-%s', + \PHP_INT_SIZE >= 8 ? dechex(substr($time, 0, -4)) : bin2hex(BinaryUtil::fromBase(substr($time, 0, -4), BinaryUtil::BASE10)), + dechex((int) substr($time, -4, 3)), + substr($uuidV1, 19) + ), '-', 8, 0)); + } + public static function generate(\DateTimeInterface $time = null): string { if (null === $mtime = $time) { From 9ee14372ab0e0059428fea69b6fa59d08fe47c64 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 19 Dec 2023 12:29:43 +0100 Subject: [PATCH 066/395] [Uid] Add `UuidV1::toV6()`, `UuidV1::toV7()` and `UuidV6::toV7()` --- src/Symfony/Component/Uid/CHANGELOG.md | 2 +- src/Symfony/Component/Uid/Tests/UuidTest.php | 37 ++++++++++++++------ src/Symfony/Component/Uid/UuidV1.php | 12 +++++++ src/Symfony/Component/Uid/UuidV6.php | 26 ++++++++++++-- src/Symfony/Component/Uid/UuidV7.php | 22 ------------ 5 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/Symfony/Component/Uid/CHANGELOG.md b/src/Symfony/Component/Uid/CHANGELOG.md index 1e07caa42a721..dd28dd6a54c49 100644 --- a/src/Symfony/Component/Uid/CHANGELOG.md +++ b/src/Symfony/Component/Uid/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 7.1 --- - * Add `UuidV6::fromV1()` and `UuidV7::fromV1()` + * Add `UuidV1::toV6()`, `UuidV1::toV7()` and `UuidV6::toV7()` 6.2 --- diff --git a/src/Symfony/Component/Uid/Tests/UuidTest.php b/src/Symfony/Component/Uid/Tests/UuidTest.php index 172bc18340950..297f85cb99993 100644 --- a/src/Symfony/Component/Uid/Tests/UuidTest.php +++ b/src/Symfony/Component/Uid/Tests/UuidTest.php @@ -428,32 +428,47 @@ public function testFromStringBase58Padding() $this->assertInstanceOf(Uuid::class, Uuid::fromString('111111111u9QRyVM94rdmZ')); } - public function testV6FromV1() + public function testV1ToV6() { $uuidV1 = new UuidV1('8189d3de-9670-11ee-b9d1-0242ac120002'); - $uuidV6 = UuidV6::fromV1($uuidV1); + $uuidV6 = $uuidV1->toV6(); $this->assertEquals($uuidV1->getDateTime(), $uuidV6->getDateTime()); $this->assertSame($uuidV1->getNode(), $uuidV6->getNode()); - $this->assertEquals($uuidV6, UuidV6::fromV1($uuidV1)); + $this->assertEquals($uuidV6, $uuidV1->toV6()); } - public function testV7FromV1BeforeUnixEpochThrows() + public function testV1ToV7BeforeUnixEpochThrows() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('UUIDv1 with a timestamp before Unix epoch cannot be converted to UUIDv7'); + $this->expectExceptionMessage('Cannot convert UUID to v7: its timestamp is before the Unix epoch.'); - UuidV7::fromV1(new UuidV1('9aba8000-ff00-11b0-b3db-3b3fc83afdfc')); // Timestamp is 1969-01-01 00:00:00.0000000 + (new UuidV1('9aba8000-ff00-11b0-b3db-3b3fc83afdfc'))->toV7(); // Timestamp is 1969-01-01 00:00:00.0000000 } - public function testV7FromV1() + public function testV1ToV7() { $uuidV1 = new UuidV1('eb248d80-ea4f-11ec-9d2a-839425e6fb88'); $sameUuidV1100NanosecondsLater = new UuidV1('eb248d81-ea4f-11ec-9d2a-839425e6fb88'); - $uuidV7 = UuidV7::fromV1($uuidV1); + $uuidV7 = $uuidV1->toV7(); + $sameUuidV7100NanosecondsLater = $sameUuidV1100NanosecondsLater->toV7(); - $this->assertEquals($uuidV1->getDateTime(), $uuidV7->getDateTime()); - $this->assertEquals($uuidV7, UuidV7::fromV1($uuidV1)); - $this->assertNotEquals($uuidV7, UuidV7::fromV1($sameUuidV1100NanosecondsLater)); + $this->assertSame($uuidV1->getDateTime()->format('Uv'), $uuidV7->getDateTime()->format('Uv')); + $this->assertEquals($uuidV7, $uuidV1->toV7()); + $this->assertNotEquals($uuidV7, $sameUuidV7100NanosecondsLater); + $this->assertSame(hexdec('0'.substr($uuidV7, -2)) + 1, hexdec('0'.substr($sameUuidV7100NanosecondsLater, -2))); + } + + public function testV1ToV7WhenExtraTimeEntropyOverflows() + { + $uuidV1 = new UuidV1('10e7718f-2d4f-11be-bfed-cdd35907e584'); + $sameUuidV1100NanosecondsLater = new UuidV1('10e77190-2d4f-11be-bfed-cdd35907e584'); + $uuidV7 = $uuidV1->toV7(); + $sameUuidV7100NanosecondsLater = $sameUuidV1100NanosecondsLater->toV7(); + + $this->assertSame($uuidV1->getDateTime()->format('Uv'), $uuidV7->getDateTime()->format('Uv')); + $this->assertEquals($uuidV7, $uuidV1->toV7()); + $this->assertNotEquals($uuidV7, $sameUuidV7100NanosecondsLater); + $this->assertSame(hexdec('0'.substr($uuidV7, -2)) + 1, hexdec('0'.substr($sameUuidV7100NanosecondsLater, -2))); } } diff --git a/src/Symfony/Component/Uid/UuidV1.php b/src/Symfony/Component/Uid/UuidV1.php index 8c03792113741..4bb24dacee87d 100644 --- a/src/Symfony/Component/Uid/UuidV1.php +++ b/src/Symfony/Component/Uid/UuidV1.php @@ -41,6 +41,18 @@ public function getNode(): string return uuid_mac($this->uid); } + public function toV6(): UuidV6 + { + $uuid = $this->uid; + + return new UuidV6(substr($uuid, 15, 3).substr($uuid, 9, 4).$uuid[0].'-'.substr($uuid, 1, 4).'-6'.substr($uuid, 5, 3).substr($uuid, 18, 6).substr($uuid, 24)); + } + + public function toV7(): UuidV7 + { + return $this->toV6()->toV7(); + } + public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string { $uuid = !$time || !$node ? uuid_create(static::TYPE) : parent::NIL; diff --git a/src/Symfony/Component/Uid/UuidV6.php b/src/Symfony/Component/Uid/UuidV6.php index bed75bcb58135..cb213ce774208 100644 --- a/src/Symfony/Component/Uid/UuidV6.php +++ b/src/Symfony/Component/Uid/UuidV6.php @@ -43,11 +43,31 @@ public function getNode(): string return substr($this->uid, 24); } - public static function fromV1(UuidV1 $uuidV1): self + public function toV7(): UuidV7 { - $uuidV1 = $uuidV1->toRfc4122(); + $uuid = $this->uid; + $time = BinaryUtil::hexToNumericString('0'.substr($uuid, 0, 8).substr($uuid, 9, 4).substr($uuid, 15, 3)); + if ('-' === $time[0]) { + throw new \InvalidArgumentException('Cannot convert UUID to v7: its timestamp is before the Unix epoch.'); + } + + $ms = \strlen($time) > 4 ? substr($time, 0, -4) : '0'; + $time = dechex(10000 * hexdec(substr($uuid, 20, 3)) + substr($time, -4)); + + if (\strlen($time) > 6) { + $uuid[29] = dechex(hexdec($uuid[29]) ^ hexdec($time[0])); + $time = substr($time, 1); + } - return new self(substr($uuidV1, 15, 3).substr($uuidV1, 9, 4).$uuidV1[0].'-'.substr($uuidV1, 1, 4).'-6'.substr($uuidV1, 5, 3).substr($uuidV1, 18, 6).substr($uuidV1, 24)); + return new UuidV7(substr_replace(sprintf( + '%012s-7%s-%s%s-%s%06s', + \PHP_INT_SIZE >= 8 ? dechex($ms) : bin2hex(BinaryUtil::fromBase($ms, BinaryUtil::BASE10)), + substr($uuid, -6, 3), + $uuid[19], + substr($uuid, -3), + substr($uuid, -12, 6), + $time + ), '-', 8, 0)); } public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string diff --git a/src/Symfony/Component/Uid/UuidV7.php b/src/Symfony/Component/Uid/UuidV7.php index ce90b2a3da111..88797d37eda67 100644 --- a/src/Symfony/Component/Uid/UuidV7.php +++ b/src/Symfony/Component/Uid/UuidV7.php @@ -49,28 +49,6 @@ public function getDateTime(): \DateTimeImmutable return \DateTimeImmutable::createFromFormat('U.v', substr_replace($time, '.', -3, 0)); } - /** - * Sub-millisecond timestamp precision is lost since UUIDv7 don't support it. - */ - public static function fromV1(UuidV1 $uuidV1): self - { - $uuidV1 = $uuidV1->toRfc4122(); - $time = '0'.substr($uuidV1, 15, 3).substr($uuidV1, 9, 4).substr($uuidV1, 0, 8); - $time = BinaryUtil::hexToNumericString($time); - if ('-' === $time[0]) { - throw new \InvalidArgumentException('UUIDv1 with a timestamp before Unix epoch cannot be converted to UUIDv7'); - } - - $time = str_pad($time, 5, '0', \STR_PAD_LEFT); - - return new self(substr_replace(sprintf( - '%012s-7%03s-%s', - \PHP_INT_SIZE >= 8 ? dechex(substr($time, 0, -4)) : bin2hex(BinaryUtil::fromBase(substr($time, 0, -4), BinaryUtil::BASE10)), - dechex((int) substr($time, -4, 3)), - substr($uuidV1, 19) - ), '-', 8, 0)); - } - public static function generate(\DateTimeInterface $time = null): string { if (null === $mtime = $time) { From 5675c95a6293e5cb7ad36a64c4b77ad3f68bfa2f Mon Sep 17 00:00:00 2001 From: Alan ZARLI Date: Mon, 18 Dec 2023 14:48:00 +0100 Subject: [PATCH 067/395] [Notifier] Add Smsbox notifier bridge --- .../FrameworkExtension.php | 1 + .../Resources/config/notifier_transports.php | 4 + .../Notifier/Bridge/Smsbox/.gitattributes | 4 + .../Notifier/Bridge/Smsbox/.gitignore | 3 + .../Notifier/Bridge/Smsbox/CHANGELOG.md | 7 + .../Component/Notifier/Bridge/Smsbox/LICENSE | 19 + .../Notifier/Bridge/Smsbox/README.md | 50 +++ .../Notifier/Bridge/Smsbox/SmsboxOptions.php | 388 ++++++++++++++++++ .../Bridge/Smsbox/SmsboxTransport.php | 160 ++++++++ .../Bridge/Smsbox/SmsboxTransportFactory.php | 51 +++ .../Bridge/Smsbox/Tests/SmsboxOptionsTest.php | 77 ++++ .../Tests/SmsboxTransportFactoryTest.php | 53 +++ .../Smsbox/Tests/SmsboxTransportTest.php | 243 +++++++++++ .../Notifier/Bridge/Smsbox/composer.json | 40 ++ .../Notifier/Bridge/Smsbox/phpunit.xml.dist | 31 ++ .../Exception/UnsupportedSchemeException.php | 4 + .../UnsupportedSchemeExceptionTest.php | 1 + src/Symfony/Component/Notifier/Transport.php | 1 + 18 files changed, 1137 insertions(+) create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/.gitattributes create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/.gitignore create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxOptionsTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportFactoryTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 17330540df606..f94430eeb0cfb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2750,6 +2750,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\Sms77\Sms77TransportFactory::class => 'notifier.transport_factory.sms77', NotifierBridge\Smsapi\SmsapiTransportFactory::class => 'notifier.transport_factory.smsapi', NotifierBridge\SmsBiuras\SmsBiurasTransportFactory::class => 'notifier.transport_factory.sms-biuras', + NotifierBridge\Smsbox\SmsboxTransportFactory::class => 'notifier.transport_factory.smsbox', NotifierBridge\Smsc\SmscTransportFactory::class => 'notifier.transport_factory.smsc', NotifierBridge\SmsFactor\SmsFactorTransportFactory::class => 'notifier.transport_factory.sms-factor', NotifierBridge\Smsmode\SmsmodeTransportFactory::class => 'notifier.transport_factory.smsmode', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index fd2952c50a9cd..a4a2574a2378e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -188,6 +188,10 @@ ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') + ->set('notifier.transport_factory.smsbox', Bridge\Smsbox\SmsboxTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ->set('notifier.transport_factory.smsc', Bridge\Smsc\SmscTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Smsbox/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/.gitignore b/src/Symfony/Component/Notifier/Bridge/Smsbox/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Smsbox/CHANGELOG.md new file mode 100644 index 0000000000000..ab7facf3a8b5c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.1 +--- + + * Add the bridge \ No newline at end of file diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/LICENSE b/src/Symfony/Component/Notifier/Bridge/Smsbox/LICENSE new file mode 100644 index 0000000000000..3ed9f412ce53d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/README.md b/src/Symfony/Component/Notifier/Bridge/Smsbox/README.md new file mode 100644 index 0000000000000..7f9b36691c754 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/README.md @@ -0,0 +1,50 @@ +SMSBOX Notifier +--------------- + +Provides [SMSBOX](https://www.smsbox.net/en/) integration for Symfony Notifier. + +DSN example +----------- + +``` +SMSBOX_DSN=smsbox://APIKEY@default?mode=MODE&strategy=STRATEGY&sender=SENDER +``` + +where: + +- `APIKEY` is your SMSBOX api key +- `MODE` is the sending mode +- `STRATEGY` is the type of your message +- `SENDER` is the sender name + +## You can add numerous options to a message + +With a SMSBOX Message, you can use the SmsboxOptions class and use the setters to add [message options](https://www.smsbox.net/en/tools-development#developer-space) + +```php +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions; + +$sms = new SmsMessage('+33123456789', 'Your %1% message %2%'); +$options = (new SmsboxOptions()) + ->mode(SmsboxOptions::MESSAGE_MODE_EXPERT) + ->strategy(SmsboxOptions::MESSAGE_STRATEGY_NOT_MARKETING_GROUP) + ->sender('Your sender') + ->date('DD/MM/YYYY') + ->hour('HH:MM') + ->coding(SmsboxOptions::MESSAGE_CODING_UNICODE) + ->charset(SmsboxOptions::MESSAGE_CHARSET_UTF8) + ->udh(SmsboxOptions::MESSAGE_UDH_DISABLED_CONCAT) + ->callback(true) + ->allowVocal(true) + ->maxParts(2) + ->validity(100) + ->daysMinMax(min: SmsboxOptions::MESSAGE_DAYS_TUESDAY, max: SmsboxOptions::MESSAGE_DAYS_FRIDAY) + ->hoursMinMax(min: 8, max: 10) + ->variable(['variable1', 'variable2']) + ->dateTime(new \DateTime()) + ->destIso('FR'); + +$sms->options($options); +$texter->send($sms); +``` \ No newline at end of file diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php new file mode 100644 index 0000000000000..d95c7b801a915 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php @@ -0,0 +1,388 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox; + +use Symfony\Component\Notifier\Exception\InvalidArgumentException; +use Symfony\Component\Notifier\Message\MessageOptionsInterface; + +/** + * @author Alan Zarli + * @author Farid Touil + */ +final class SmsboxOptions implements MessageOptionsInterface +{ + public const MESSAGE_MODE_STANDARD = 'Standard'; + public const MESSAGE_MODE_EXPERT = 'Expert'; + public const MESSAGE_MODE_RESPONSE = 'Reponse'; + + public const MESSAGE_STRATEGY_PRIVATE = 1; + public const MESSAGE_STRATEGY_NOTIFICATION = 2; + public const MESSAGE_STRATEGY_NOT_MARKETING_GROUP = 3; + public const MESSAGE_STRATEGY_MARKETING = 4; + + public const MESSAGE_CODING_DEFAULT = 'default'; + public const MESSAGE_CODING_UNICODE = 'unicode'; + public const MESSAGE_CODING_AUTO = 'auto'; + + public const MESSAGE_CHARSET_ISO_1 = 'iso-8859-1'; + public const MESSAGE_CHARSET_ISO_15 = 'iso-8859-15'; + public const MESSAGE_CHARSET_UTF8 = 'utf-8'; + + public const MESSAGE_DAYS_MONDAY = 1; + public const MESSAGE_DAYS_TUESDAY = 2; + public const MESSAGE_DAYS_WEDNESDAY = 3; + public const MESSAGE_DAYS_THURSDAY = 4; + public const MESSAGE_DAYS_FRIDAY = 5; + public const MESSAGE_DAYS_SATURDAY = 6; + public const MESSAGE_DAYS_SUNDAY = 7; + + public const MESSAGE_UDH_6_OCTETS = 1; + public const MESSAGE_UDH_7_OCTETS = 2; + public const MESSAGE_UDH_DISABLED_CONCAT = 0; + + private array $options; + + public function __construct(array $options = []) + { + $this->options = []; + } + + /** + * @return null + */ + public function getRecipientId(): ?string + { + return null; + } + + public function mode(string $mode) + { + $this->options['mode'] = self::validateMode($mode); + + return $this; + } + + public function strategy(int $strategy) + { + $this->options['strategy'] = self::validateStrategy($strategy); + + return $this; + } + + public function date(string $date) + { + $this->options['date'] = self::validateDate($date); + + return $this; + } + + public function hour(string $hour) + { + $this->options['heure'] = self::validateHour($hour); + + return $this; + } + + /** + * This method mustn't be set along with date and hour methods. + */ + public function dateTime(\DateTime $dateTime) + { + $this->options['dateTime'] = self::validateDateTime($dateTime); + + return $this; + } + + /** + * This method wait an ISO 3166-1 alpha. + */ + public function destIso(string $isoCode) + { + $this->options['dest_iso'] = self::validateDestIso($isoCode); + + return $this; + } + + /** + * This method will automatically set personnalise = 1 (according to SMSBOX documentation). + */ + public function variable(array $variable) + { + $this->options['variable'] = $variable; + + return $this; + } + + public function coding(string $coding) + { + $this->options['coding'] = self::validateCoding($coding); + + return $this; + } + + public function charset(string $charset) + { + $this->options['charset'] = self::validateCharset($charset); + + return $this; + } + + public function udh(int $udh) + { + $this->options['udh'] = self::validateUdh($udh); + + return $this; + } + + /** + * The true value = 1 in SMSBOX documentation. + */ + public function callback(bool $callback) + { + $this->options['callback'] = $callback; + + return $this; + } + + /** + * The true value = 1 in SMSBOX documentation. + */ + public function allowVocal(bool $allowVocal) + { + $this->options['allow_vocal'] = $allowVocal; + + return $this; + } + + public function maxParts(int $maxParts) + { + $this->options['max_parts'] = self::validateMaxParts($maxParts); + + return $this; + } + + public function validity(int $validity) + { + $this->options['validity'] = self::validateValidity($validity); + + return $this; + } + + public function daysMinMax(int $min, int $max) + { + $this->options['daysMinMax'] = self::validateDays($min, $max); + + return $this; + } + + public function hoursMinMax(int $min, int $max) + { + $this->options['hoursMinMax'] = self::validateHours($min, $max); + + return $this; + } + + public function sender(string $sender) + { + $this->options['sender'] = $sender; + + return $this; + } + + public function toArray(): array + { + return $this->options; + } + + public static function validateMode(string $mode): string + { + $supportedModes = [ + self::MESSAGE_MODE_STANDARD, + self::MESSAGE_MODE_EXPERT, + self::MESSAGE_MODE_RESPONSE, + ]; + + if (!\in_array($mode, $supportedModes, true)) { + throw new InvalidArgumentException(sprintf('The message mode "%s" is not supported; supported message modes are: "%s".', $mode, implode('", "', $supportedModes))); + } + + return $mode; + } + + public static function validateStrategy(int $strategy): int + { + $supportedStrategies = [ + self::MESSAGE_STRATEGY_PRIVATE, + self::MESSAGE_STRATEGY_NOTIFICATION, + self::MESSAGE_STRATEGY_NOT_MARKETING_GROUP, + self::MESSAGE_STRATEGY_MARKETING, + ]; + if (!\in_array($strategy, $supportedStrategies, true)) { + throw new InvalidArgumentException(sprintf('The message strategy "%s" is not supported; supported strategies types are: "%s".', $strategy, implode('", "', $supportedStrategies))); + } + + return $strategy; + } + + public static function validateDate(string $date): string + { + $dateTimeObj = \DateTime::createFromFormat('d/m/Y', $date); + $now = new \DateTime(); + $tz = new \DateTimeZone('Europe/Paris'); + $dateTimeObj->setTimezone($tz); + $now->setTimezone($tz); + + if (!$dateTimeObj || $dateTimeObj->format('Y-m-d') <= (new \DateTime())->format('Y-m-d')) { + throw new InvalidArgumentException('The date must be in DD/MM/YYYY format and greater than the current date.'); + } + + return $date; + } + + public static function validateDateTime(\DateTime $dateTime) + { + \Locale::setDefault('fr'); + $now = new \DateTime(); + $tz = new \DateTimeZone('Europe/Paris'); + $now->setTimezone($tz); + $dateTime->setTimezone($tz); + + if ($now > $dateTime || $dateTime > $now->modify('+2 Year')) { + throw new InvalidArgumentException('dateTime must be greater to the actual date and limited to 2 years in the future.'); + } + + return $dateTime; + } + + public static function validateDestIso(string $isoCode) + { + if (!preg_match('/^[a-z]{2}$/i', $isoCode)) { + throw new InvalidArgumentException('destIso must be the ISO 3166-1 alpha 2 on two uppercase characters.'); + } + + return $isoCode; + } + + public static function validateHour(string $hour): string + { + $dateTimeObjhour = \DateTime::createFromFormat('H:i', $hour); + + if (!$dateTimeObjhour || $dateTimeObjhour->format('H:i') != $hour) { + throw new InvalidArgumentException('Hour must be in HH:MM format and valid.'); + } + + return $hour; + } + + public static function validateCoding(string $coding): string + { + $supportedCodings = [ + self::MESSAGE_CODING_DEFAULT, + self::MESSAGE_CODING_UNICODE, + self::MESSAGE_CODING_AUTO, + ]; + + if (!\in_array($coding, $supportedCodings, true)) { + throw new InvalidArgumentException(sprintf('The message coding : "%s" is not supported; supported codings types are: "%s".', $coding, implode('", "', $supportedCodings))); + } + + return $coding; + } + + public static function validateCharset(string $charset): string + { + $supportedCharsets = [ + self::MESSAGE_CHARSET_ISO_1, + self::MESSAGE_CHARSET_ISO_15, + self::MESSAGE_CHARSET_UTF8, + ]; + + if (!\in_array($charset, $supportedCharsets, true)) { + throw new InvalidArgumentException(sprintf('The message charset : "%s" is not supported; supported charsets types are: "%s".', $charset, implode('", "', $supportedCharsets))); + } + + return $charset; + } + + public static function validateUdh(int $udh): int + { + $supportedUdhs = [ + self::MESSAGE_UDH_6_OCTETS, + self::MESSAGE_UDH_7_OCTETS, + self::MESSAGE_UDH_DISABLED_CONCAT, + ]; + + if (!\in_array($udh, $supportedUdhs, true)) { + throw new InvalidArgumentException(sprintf('The message charset : "%s" is not supported; supported charsets types are: "%s".', $udh, implode('", "', $supportedUdhs))); + } + + return $udh; + } + + public static function validateMaxParts(int $maxParts): int + { + if ($maxParts < 1 || $maxParts > 8) { + throw new InvalidArgumentException(sprintf('The message max_parts : "%s" is not supported; supported max_parts values are integers between 1 and 8.', $maxParts)); + } + + return $maxParts; + } + + public static function validateValidity(int $validity): int + { + if ($validity < 5 || $validity > 1440) { + throw new InvalidArgumentException(sprintf('The message validity : "%s" is not supported; supported validity values are integers between 5 and 1440.', $validity)); + } + + return $validity; + } + + public static function validateDays(int $min, int $max): array + { + $supportedDays = [ + self::MESSAGE_DAYS_MONDAY, + self::MESSAGE_DAYS_TUESDAY, + self::MESSAGE_DAYS_WEDNESDAY, + self::MESSAGE_DAYS_THURSDAY, + self::MESSAGE_DAYS_FRIDAY, + self::MESSAGE_DAYS_SATURDAY, + self::MESSAGE_DAYS_SUNDAY, + ]; + + if (!\in_array($min, $supportedDays, true)) { + throw new InvalidArgumentException(sprintf('The message min : "%s" is not supported; supported charsets types are: "%s".', $min, implode('", "', $supportedDays))); + } + + if (!\in_array($max, $supportedDays, true)) { + throw new InvalidArgumentException(sprintf('The message max : "%s" is not supported; supported charsets types are: "%s".', $max, implode('", "', $supportedDays))); + } + + if ($min > $max) { + throw new InvalidArgumentException(sprintf('The message max must be greater than min.', $min)); + } + + return [$min, $max]; + } + + public static function validateHours(int $min, int $max): array + { + if ($min < 0 || $min > $max) { + throw new InvalidArgumentException(sprintf('The message min : "%s" is not supported; supported min values are integers between 0 and 23.', $min)); + } + + if ($max > 23) { + throw new InvalidArgumentException(sprintf('The message max : "%s" is not supported; supported min values are integers between 0 and 23.', $max)); + } + + return [$min, $max]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransport.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransport.php new file mode 100644 index 0000000000000..899acaddd328c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransport.php @@ -0,0 +1,160 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox; + +use Symfony\Component\Notifier\Exception\InvalidArgumentException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Alan Zarli + * @author Farid Touil + */ +final class SmsboxTransport extends AbstractTransport +{ + protected const HOST = 'api.smsbox.pro'; + + private string $apiKey; + private ?string $mode; + private ?int $strategy; + private ?string $sender; + + public function __construct(#[\SensitiveParameter] string $apiKey, string $mode, int $strategy, ?string $sender, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->apiKey = $apiKey; + $this->mode = $mode; + $this->strategy = $strategy; + $this->sender = $sender; + + SmsboxOptions::validateMode($this->mode); + SmsboxOptions::validateStrategy($this->strategy); + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + if (SmsboxOptions::MESSAGE_MODE_EXPERT === $this->mode) { + return sprintf('smsbox://%s?mode=%s&strategy=%s&sender=%s', $this->getEndpoint(), $this->mode, $this->strategy, $this->sender); + } + + return sprintf('smsbox://%s?mode=%s&strategy=%s', $this->getEndpoint(), $this->mode, $this->strategy); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof SmsMessage && (null === $message->getOptions() || $message->getOptions() instanceof SmsboxOptions); + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof SmsMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, SmsMessage::class, $message); + } + + $phoneCleaned = preg_replace('/[^0-9+]+/', '', $message->getPhone()); + if (!preg_match("/^(\+|)[0-9]{7,14}$/", $phoneCleaned)) { + throw new InvalidArgumentException('Invalid phone number.'); + } + + $options = $message->getOptions()?->toArray() ?? []; + $options['dest'] = $phoneCleaned; + $options['msg'] = $message->getSubject(); + $options['id'] = 1; + $options['usage'] = 'symfony'; + $options['mode'] ??= $this->mode; + $options['strategy'] ??= $this->strategy; + + if (SmsboxOptions::MESSAGE_MODE_EXPERT === $options['mode']) { + $options['origine'] = $options['sender'] ?? $this->sender; + } + unset($options['sender']); + + if (isset($options['daysMinMax'])) { + $options['day_min'] = $options['daysMinMax'][0]; + $options['day_max'] = $options['daysMinMax'][1]; + unset($options['daysMinMax']); + } + + if (isset($options['hoursMinMax'])) { + $options['hour_min'] = $options['hoursMinMax'][0]; + $options['hour_max'] = $options['hoursMinMax'][1]; + unset($options['hoursMinMax']); + } + + if (isset($options['dateTime'])) { + if (isset($options['heure']) || isset($options['date'])) { + throw new InvalidArgumentException("You mustn't set the dateTime method along with date or hour methods."); + } + + $options['date'] = $options['dateTime']->format('d/m/Y'); + $options['heure'] = $options['dateTime']->format('H:i'); + unset($options['dateTime']); + } + + if (isset($options['variable'])) { + preg_match_all('%([0-9]+)%', $options['msg'], $matches); + $occurenceValMsg = $matches[0]; + $occurenceValMsgMax = max($occurenceValMsg); + + if ($occurenceValMsgMax != \count($options['variable'])) { + throw new InvalidArgumentException('You must have the same amount of index in your array as you have variable.'); + } + + $t = str_replace([',', ';'], ['%d44%', '%d59%'], $options['variable']); + $variableStr = implode(';', $t); + $options['dest'] .= ';'.$variableStr; + $options['personnalise'] = 1; + unset($options['variable']); + } + + $response = $this->client->request('POST', sprintf('https://%s/1.1/api.php', $this->getEndpoint()), [ + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Authorization' => 'App '.$this->apiKey, + ], + 'body' => $options, + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote Smsbox server.', $response, 0, $e); + } + + if (200 !== $statusCode) { + $error = $response->getContent(false); + throw new TransportException(sprintf('Unable to send the SMS: "%s" (%s).', $error['description'], $error['code']), $response); + } + + $body = $response->getContent(false); + if (!preg_match('/^OK .*/', $body)) { + throw new TransportException(sprintf('Unable to send the SMS: "%s" (%s).', $body, 400), $response); + } + + if (!preg_match('/^OK (\d+)/', $body, $reference)) { + throw new TransportException(sprintf('Unable to send the SMS: "%s" (%s).', $body, 400), $response); + } + + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($reference[1]); + + return $sentMessage; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransportFactory.php new file mode 100644 index 0000000000000..9a1f386581a86 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransportFactory.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Alan Zarli + * @author Farid Touil + */ +final class SmsboxTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): SmsboxTransport + { + $scheme = $dsn->getScheme(); + + if ('smsbox' !== $scheme) { + throw new UnsupportedSchemeException($dsn, 'smsbox', $this->getSupportedSchemes()); + } + + $apiKey = $this->getUser($dsn); + $mode = $dsn->getRequiredOption('mode'); + $strategy = $dsn->getRequiredOption('strategy'); + $sender = $dsn->getOption('sender'); + + if (SmsboxOptions::MESSAGE_MODE_EXPERT === $mode) { + $sender = $dsn->getRequiredOption('sender'); + } + + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new SmsboxTransport($apiKey, $mode, $strategy, $sender, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['smsbox']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxOptionsTest.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxOptionsTest.php new file mode 100644 index 0000000000000..353dba391efe5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxOptionsTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions; +use Symfony\Component\Notifier\Exception\InvalidArgumentException; + +class SmsboxOptionsTest extends TestCase +{ + public function testSmsboxOptions() + { + $smsboxOptions = (new SmsboxOptions()) + ->mode(SmsboxOptions::MESSAGE_MODE_EXPERT) + ->sender('SENDER') + ->strategy(SmsboxOptions::MESSAGE_STRATEGY_MARKETING) + ->charset(SmsboxOptions::MESSAGE_CHARSET_UTF8) + ->udh(SmsboxOptions::MESSAGE_UDH_DISABLED_CONCAT) + ->maxParts(2) + ->validity(100) + ->destIso('FR'); + + self::assertSame([ + 'mode' => 'Expert', + 'sender' => 'SENDER', + 'strategy' => 4, + 'charset' => 'utf-8', + 'udh' => 0, + 'max_parts' => 2, + 'validity' => 100, + 'dest_iso' => 'FR', + ], $smsboxOptions->toArray()); + } + + public function testSmsboxOptionsInvalidMode() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The message mode "XXXXXX" is not supported; supported message modes are: "Standard", "Expert", "Reponse"'); + + $smsboxOptions = (new SmsboxOptions()) + ->mode('XXXXXX') + ->sender('SENDER') + ->strategy(SmsboxOptions::MESSAGE_STRATEGY_MARKETING); + } + + public function testSmsboxOptionsInvalidStrategy() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The message strategy "10" is not supported; supported strategies types are: "1", "2", "3", "4"'); + + $smsboxOptions = (new SmsboxOptions()) + ->mode(SmsboxOptions::MESSAGE_MODE_STANDARD) + ->sender('SENDER') + ->strategy(10); + } + + public function testSmsboxOptionsInvalidDestIso() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('destIso must be the ISO 3166-1 alpha 2 on two uppercase characters.'); + + $smsboxOptions = (new SmsboxOptions()) + ->mode(SmsboxOptions::MESSAGE_MODE_EXPERT) + ->sender('SENDER') + ->strategy(SmsboxOptions::MESSAGE_STRATEGY_MARKETING) + ->destIso('X1'); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportFactoryTest.php new file mode 100644 index 0000000000000..5e2596e0fffe0 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportFactoryTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox\Tests; + +use Symfony\Component\Notifier\Bridge\Smsbox\SmsboxTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; + +final class SmsboxTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): SmsboxTransportFactory + { + return new SmsboxTransportFactory(); + } + + public static function createProvider(): iterable + { + yield ['smsbox://host.test?mode=Standard&strategy=4', 'smsbox://APIKEY@host.test?mode=Standard&strategy=4']; + yield ['smsbox://host.test?mode=Expert&strategy=4&sender=SENDER', 'smsbox://APIKEY@host.test?mode=Expert&strategy=4&sender=SENDER']; + } + + public static function incompleteDsnProvider(): iterable + { + yield ['smsbox://APIKEY@host.test?strategy=4&sender=SENDER']; + yield ['smsbox://APIKEY@host.test?mode=Standard&sender=SENDER']; + } + + public static function supportsProvider(): iterable + { + yield [true, 'smsbox://APIKEY@host.test?mode=MODE&strategy=STRATEGY&sender=SENDER']; + yield [false, 'somethingElse://APIKEY@host.test?mode=MODE&strategy=STRATEGY&sender=SENDER']; + } + + public static function missingRequiredOptionProvider(): iterable + { + yield ['smsbox://apiKey@host.test?strategy=4']; + yield ['smsbox://apiKey@host.test?mode=Standard']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://APIKEY@host.test?mode=MODE&strategy=STRATEGY&sender=SENDER']; + yield ['somethingElse://APIKEY@host.test']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportTest.php new file mode 100644 index 0000000000000..0ab5c763d72ed --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportTest.php @@ -0,0 +1,243 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions; +use Symfony\Component\Notifier\Bridge\Smsbox\SmsboxTransport; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Transport\DummyMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class SmsboxTransportTest extends TransportTestCase +{ + public static function createTransport(HttpClientInterface $client = null): SmsboxTransport + { + return new SmsboxTransport('apikey', 'Standard', 4, null, $client ?? new MockHttpClient()); + } + + public static function toStringProvider(): iterable + { + yield ['smsbox://api.smsbox.pro?mode=Standard&strategy=4', self::createTransport()]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('+33612345678', 'Hello!')]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + yield [new DummyMessage()]; + } + + public function testBasicQuerySucceded() + { + $message = new SmsMessage('+33612345678', 'Hello!'); + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects(self::once()) + ->method('getContent') + ->willReturn('OK 12345678'); + + $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { + self::assertSame('POST', $method); + self::assertSame('https://api.smsbox.pro/1.1/api.php', $url); + self::assertSame('dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4', $request['body']); + + return $response; + }); + + $transport = $this->createTransport($client); + $sentMessage = $transport->send($message); + + self::assertSame('12345678', $sentMessage->getMessageId()); + } + + public function testBasicQueryFailed() + { + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Unable to send the SMS: "ERROR 02" (400).'); + + $message = new SmsMessage('+33612345678', 'Hello!'); + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects(self::once()) + ->method('getContent') + ->willReturn('ERROR 02'); + + $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { + self::assertSame('POST', $method); + self::assertSame('https://api.smsbox.pro/1.1/api.php', $url); + self::assertSame('dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4', $request['body']); + + return $response; + }); + + $transport = $this->createTransport($client); + $transport->send($message); + } + + public function testQuerySuccededWithOptions() + { + $message = new SmsMessage('+33612345678', 'Hello!'); + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects(self::once()) + ->method('getContent') + ->willReturn('OK 12345678'); + + $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { + self::assertSame('POST', $method); + self::assertSame('https://api.smsbox.pro/1.1/api.php', $url); + self::assertSame('max_parts=5&coding=unicode&callback=1&dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4&day_min=1&day_max=3', $request['body']); + + return $response; + }); + + $transport = $this->createTransport($client); + $options = (new SmsboxOptions()) + ->maxParts(5) + ->coding(SmsboxOptions::MESSAGE_CODING_UNICODE) + ->daysMinMax(1, 3) + ->callback(true); + + $message->options($options); + $sentMessage = $transport->send($message); + + self::assertSame('12345678', $sentMessage->getMessageId()); + } + + public function testQueryDateTime() + { + $message = new SmsMessage('+33612345678', 'Hello!'); + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects(self::once()) + ->method('getContent') + ->willReturn('OK 12345678'); + + $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { + self::assertSame('POST', $method); + self::assertSame('https://api.smsbox.pro/1.1/api.php', $url); + self::assertSame('dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4&date=05%2F12%2F2025&heure=19%3A00', $request['body']); + + return $response; + }); + + $dateTime = \DateTime::createFromFormat('d/m/Y H:i', '05/12/2025 18:00', new \DateTimeZone('UTC')); + + $transport = $this->createTransport($client); + + $options = (new SmsboxOptions()) + ->dateTime($dateTime); + + $message->options($options); + $sentMessage = $transport->send($message); + + self::assertSame('12345678', $sentMessage->getMessageId()); + } + + public function testQueryVariable() + { + $message = new SmsMessage('0612345678', 'Hello %1% %2%'); + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects(self::once()) + ->method('getContent') + ->willReturn('OK 12345678'); + + $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { + self::assertSame('POST', $method); + self::assertSame('https://api.smsbox.pro/1.1/api.php', $url); + self::assertSame('dest=0612345678%3Btye%25d44%25%25d44%25t%3Be%25d59%25%25d44%25fe&msg=Hello+%251%25+%252%25&id=1&usage=symfony&mode=Standard&strategy=4&personnalise=1', $request['body']); + + return $response; + }); + + $transport = $this->createTransport($client); + + $options = (new SmsboxOptions()) + ->variable(['tye,,t', 'e;,fe']); + + $message->options($options); + $sentMessage = $transport->send($message); + + self::assertSame('12345678', $sentMessage->getMessageId()); + } + + public function testSmsboxOptionsInvalidDateTimeAndDate() + { + $response = $this->createMock(ResponseInterface::class); + $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { + return $response; + }); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("You mustn't set the dateTime method along with date or hour methods"); + $dateTime = \DateTime::createFromFormat('d/m/Y H:i', '01/11/2024 18:00', new \DateTimeZone('UTC')); + $message = new SmsMessage('+33612345678', 'Hello'); + + $smsboxOptions = (new SmsboxOptions()) + ->mode(SmsboxOptions::MESSAGE_MODE_EXPERT) + ->sender('SENDER') + ->strategy(SmsboxOptions::MESSAGE_STRATEGY_MARKETING) + ->dateTime($dateTime) + ->date('01/01/2024'); + + $transport = $this->createTransport($client); + + $message->options($smsboxOptions); + $transport->send($message); + } + + public function testSmsboxInvalidPhoneNumber() + { + $response = $this->createMock(ResponseInterface::class); + $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { + return $response; + }); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid phone number'); + $message = new SmsMessage('+336123456789000000', 'Hello'); + + $smsboxOptions = (new SmsboxOptions()) + ->mode(SmsboxOptions::MESSAGE_MODE_EXPERT) + ->sender('SENDER') + ->strategy(SmsboxOptions::MESSAGE_STRATEGY_MARKETING); + $transport = $this->createTransport($client); + + $message->options($smsboxOptions); + $transport->send($message); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json b/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json new file mode 100644 index 0000000000000..fedc96515d011 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json @@ -0,0 +1,40 @@ +{ + "name": "symfony/smsbox-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony Smsbox Notifier Bridge", + "keywords": ["sms", "Smsbox", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Alan Zarli", + "email": "azarli@smsbox.fr" + }, + { + "name": "Farid Touil", + "email": "ftouil@smsbox.fr" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^7.1" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\Smsbox\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Smsbox/phpunit.xml.dist new file mode 100644 index 0000000000000..ecacbe3789c67 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index c296b41730776..cef748d006108 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -240,6 +240,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\SmsBiuras\SmsBiurasTransportFactory::class, 'package' => 'symfony/sms-biuras-notifier', ], + 'smsbox' => [ + 'class' => Bridge\Smsbox\SmsboxTransportFactory::class, + 'package' => 'symfony/smsbox-notifier', + ], 'smsc' => [ 'class' => Bridge\Smsc\SmscTransportFactory::class, 'package' => 'symfony/smsc-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index c81fe985baba1..99236f6feecae 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -82,6 +82,7 @@ public static function setUpBeforeClass(): void Bridge\Sms77\Sms77TransportFactory::class => false, Bridge\Smsapi\SmsapiTransportFactory::class => false, Bridge\SmsBiuras\SmsBiurasTransportFactory::class => false, + Bridge\Smsbox\SmsboxTransportFactory::class => false, Bridge\Smsc\SmscTransportFactory::class => false, Bridge\SmsFactor\SmsFactorTransportFactory::class => false, Bridge\Smsmode\SmsmodeTransportFactory::class => false, diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index 0489703db2cd0..e7d238de86461 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -83,6 +83,7 @@ final class Transport Bridge\Sms77\Sms77TransportFactory::class, Bridge\Smsapi\SmsapiTransportFactory::class, Bridge\SmsBiuras\SmsBiurasTransportFactory::class, + Bridge\Smsbox\SmsboxTransportFactory::class, Bridge\Smsc\SmscTransportFactory::class, Bridge\SmsFactor\SmsFactorTransportFactory::class, Bridge\Smsmode\SmsmodeTransportFactory::class, From deed930c76878737d042ce148d571c923083717e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 23 Dec 2023 18:04:51 +0100 Subject: [PATCH 068/395] Reduce log level --- .../Component/Security/Http/Firewall/ContextListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index 7df6899ee9b81..a629c0402fecd 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -234,7 +234,7 @@ protected function refreshUser(TokenInterface $token): ?TokenInterface } catch (UnsupportedUserException) { // let's try the next user provider } catch (UserNotFoundException $e) { - $this->logger?->warning('Username could not be found in the selected user provider.', ['username' => $e->getUserIdentifier(), 'provider' => $provider::class]); + $this->logger?->info('Username could not be found in the selected user provider.', ['username' => $e->getUserIdentifier(), 'provider' => $provider::class]); $userNotFoundByProvider = true; } From 39e3ba4426a55416aeb36b9923d3fde891c9e4d2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 24 Dec 2023 11:13:47 +0100 Subject: [PATCH 069/395] Fix typo --- src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json b/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json index fedc96515d011..63dfca4eb99d5 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json @@ -2,7 +2,7 @@ "name": "symfony/smsbox-notifier", "type": "symfony-notifier-bridge", "description": "Symfony Smsbox Notifier Bridge", - "keywords": ["sms", "Smsbox", "notifier"], + "keywords": ["sms", "smsbox", "notifier"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ From 6dbf0e08ce2d19e3b105b6f9130bb22408e6e6c8 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 25 Dec 2023 23:40:50 +0100 Subject: [PATCH 070/395] fix return type --- .../Component/Notifier/Bridge/Smsbox/SmsboxOptions.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php index d95c7b801a915..031509587aed6 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php @@ -56,10 +56,7 @@ public function __construct(array $options = []) $this->options = []; } - /** - * @return null - */ - public function getRecipientId(): ?string + public function getRecipientId(): null { return null; } From 33d71aa7b7c368e9752198413c9fbd0ca8d82617 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 1 Nov 2023 09:14:07 +0100 Subject: [PATCH 071/395] [Tests] Streamline --- .../Command/UserPasswordHashCommandTest.php | 3 +- .../MessageDigestPasswordHasherTest.php | 7 +- .../Hasher/PasswordHasherFactoryTest.php | 4 +- .../Tests/Hasher/Pbkdf2PasswordHasherTest.php | 7 +- .../Tests/Hasher/SodiumPasswordHasherTest.php | 4 +- .../Component/Process/Tests/ProcessTest.php | 54 +++++--- .../PropertyAccessorArrayAccessTestCase.php | 3 +- .../PropertyAccessorCollectionTestCase.php | 8 +- .../Tests/PropertyAccessorTest.php | 121 +++++++++++------- .../Tests/PropertyPathBuilderTest.php | 2 +- .../PropertyAccess/Tests/PropertyPathTest.php | 20 ++- .../Tests/Policy/TokenBucketLimiterTest.php | 8 +- .../Tests/RateLimiterFactoryTest.php | 2 +- .../Tests/Generator/UrlGeneratorTest.php | 85 +++++++----- .../Routing/Tests/Loader/ObjectLoaderTest.php | 18 ++- .../Tests/Loader/XmlFileLoaderTest.php | 24 +++- .../Tests/Loader/YamlFileLoaderTest.php | 17 ++- .../Dumper/CompiledUrlMatcherDumperTest.php | 6 +- .../Matcher/RedirectableUrlMatcherTest.php | 4 +- .../Routing/Tests/Matcher/UrlMatcherTest.php | 44 +++++-- .../Routing/Tests/RouteCompilerTest.php | 19 ++- .../Component/Routing/Tests/RouteTest.php | 4 +- .../Component/Routing/Tests/RouterTest.php | 2 +- .../RememberMe/InMemoryTokenProviderTest.php | 7 +- .../Tests/Authorization/Voter/VoterTest.php | 3 +- .../Core/Tests/User/ChainUserProviderTest.php | 8 +- .../Tests/User/InMemoryUserCheckerTest.php | 3 +- .../Tests/User/InMemoryUserProviderTest.php | 7 +- .../UserPasswordValidatorTestCase.php | 3 +- .../TokenStorage/SessionTokenStorageTest.php | 4 +- .../AccessToken/Oidc/OidcTokenHandlerTest.php | 12 +- .../Oidc/OidcUserInfoTokenHandlerTest.php | 11 +- .../ChainedAccessTokenExtractorsTest.php | 6 +- ...ncodedBodyAccessTokenAuthenticatorTest.php | 6 +- .../HeaderAccessTokenAuthenticatorTest.php | 6 +- .../QueryAccessTokenAuthenticatorTest.php | 6 +- .../AccessTokenAuthenticatorTest.php | 6 +- .../FormLoginAuthenticatorTest.php | 28 ++-- .../JsonLoginAuthenticatorTest.php | 6 +- .../LoginLinkAuthenticatorTest.php | 7 +- .../RememberMeAuthenticatorTest.php | 6 +- .../CheckCredentialsListenerTest.php | 13 +- .../CsrfProtectionListenerTest.php | 7 +- .../IsGrantedAttributeListenerTest.php | 30 +++-- .../Tests/Firewall/AccessListenerTest.php | 6 +- .../Tests/Firewall/LogoutListenerTest.php | 7 +- .../Tests/Firewall/SwitchUserListenerTest.php | 20 ++- .../Security/Http/Tests/HttpUtilsTest.php | 7 +- .../Tests/Logout/LogoutUrlGeneratorTest.php | 11 +- .../PersistentRememberMeHandlerTest.php | 10 +- .../SessionAuthenticationStrategyTest.php | 6 +- .../Tests/Annotation/SerializedNameTest.php | 4 +- .../SerializerPassTest.php | 12 +- .../Tests/Encoder/JsonDecodeTest.php | 4 +- .../Tests/Encoder/JsonEncoderTest.php | 3 +- .../Factory/CacheMetadataFactoryTest.php | 3 +- .../CompiledClassMetadataFactoryTest.php | 6 +- .../Mapping/Loader/YamlFileLoaderTest.php | 4 +- .../AbstractObjectNormalizerTest.php | 41 ++++-- .../Normalizer/DataUriNormalizerTest.php | 4 +- .../Normalizer/GetSetMethodNormalizerTest.php | 9 +- .../JsonSerializableNormalizerTest.php | 3 +- .../Tests/Normalizer/ObjectNormalizerTest.php | 10 +- .../Normalizer/PropertyNormalizerTest.php | 5 +- .../Serializer/Tests/SerializerTest.php | 56 ++++++-- .../Stopwatch/Tests/StopwatchEventTest.php | 4 +- .../Stopwatch/Tests/StopwatchTest.php | 12 +- .../Tests/PhraseProviderFactoryTest.php | 24 ++-- .../Phrase/Tests/PhraseProviderTest.php | 40 +++--- .../Tests/Command/XliffLintCommandTest.php | 3 +- .../TranslationExtractorPassTest.php | 6 +- .../Tests/Formatter/IntlFormatterTest.php | 18 ++- .../Tests/Loader/CsvFileLoaderTest.php | 10 +- .../Tests/Loader/IcuDatFileLoaderTest.php | 8 +- .../Tests/Loader/IcuResFileLoaderTest.php | 8 +- .../Tests/Loader/IniFileLoaderTest.php | 5 +- .../Tests/Loader/JsonFileLoaderTest.php | 10 +- .../Tests/Loader/MoFileLoaderTest.php | 10 +- .../Tests/Loader/PhpFileLoaderTest.php | 10 +- .../Tests/Loader/PoFileLoaderTest.php | 5 +- .../Tests/Loader/QtFileLoaderTest.php | 18 +-- .../Tests/Loader/XliffFileLoaderTest.php | 25 ++-- .../Tests/Loader/YamlFileLoaderTest.php | 12 +- .../Tests/MessageCatalogueTest.php | 12 +- .../Translation/Tests/TranslatorTest.php | 38 +++--- .../AbstractComparisonValidatorTestCase.php | 6 +- .../Constraints/CssColorValidatorTest.php | 15 ++- .../Tests/Constraints/EmailValidatorTest.php | 5 +- .../Validator/Tests/Constraints/FileTest.php | 4 +- .../Constraints/FileValidatorTestCase.php | 3 +- ...idatorWithPositiveOrZeroConstraintTest.php | 6 +- ...hanValidatorWithPositiveConstraintTest.php | 4 - .../Tests/Constraints/LengthValidatorTest.php | 20 +-- ...idatorWithNegativeOrZeroConstraintTest.php | 4 - ...hanValidatorWithNegativeConstraintTest.php | 4 - .../Tests/Constraints/LocaleValidatorTest.php | 2 +- .../Tests/Constraints/UuidValidatorTest.php | 3 +- .../Validator/Tests/Constraints/WhenTest.php | 4 +- ...ontainerConstraintValidatorFactoryTest.php | 4 +- .../AddConstraintValidatorsPassTest.php | 6 +- .../Tests/Mapping/ClassMetadataTest.php | 8 +- .../Factory/BlackHoleMetadataFactoryTest.php | 7 +- .../LazyLoadingMetadataFactoryTest.php | 7 +- .../Mapping/Loader/YamlFileLoaderTest.php | 3 +- .../Validator/RecursiveValidatorTest.php | 13 +- .../Tests/Client/RequestParserTest.php | 5 +- .../Workflow/Tests/DefinitionTest.php | 6 +- .../Component/Workflow/Tests/RegistryTest.php | 8 +- .../Tests/Validator/WorkflowValidatorTest.php | 5 +- .../Yaml/Tests/Command/LintCommandTest.php | 3 +- .../Component/Yaml/Tests/InlineTest.php | 12 +- .../Component/Yaml/Tests/ParserTest.php | 94 ++++++++------ .../Service/Test/ServiceLocatorTestCase.php | 16 ++- .../Translation/Test/TranslatorTest.php | 3 +- 114 files changed, 858 insertions(+), 582 deletions(-) diff --git a/src/Symfony/Component/PasswordHasher/Tests/Command/UserPasswordHashCommandTest.php b/src/Symfony/Component/PasswordHasher/Tests/Command/UserPasswordHashCommandTest.php index 253403d218205..819a92899962b 100644 --- a/src/Symfony/Component/PasswordHasher/Tests/Command/UserPasswordHashCommandTest.php +++ b/src/Symfony/Component/PasswordHasher/Tests/Command/UserPasswordHashCommandTest.php @@ -277,10 +277,11 @@ public function testNonInteractiveEncodePasswordUsesFirstUserClass() public function testThrowsExceptionOnNoConfiguredHashers() { + $tester = new CommandTester(new UserPasswordHashCommand($this->getMockBuilder(PasswordHasherFactoryInterface::class)->getMock(), [])); + $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('There are no configured password hashers for the "security" extension.'); - $tester = new CommandTester(new UserPasswordHashCommand($this->getMockBuilder(PasswordHasherFactoryInterface::class)->getMock(), [])); $tester->execute([ 'password' => 'password', ], ['interactive' => false]); diff --git a/src/Symfony/Component/PasswordHasher/Tests/Hasher/MessageDigestPasswordHasherTest.php b/src/Symfony/Component/PasswordHasher/Tests/Hasher/MessageDigestPasswordHasherTest.php index 6abcb797b9c27..2e7a192bf8ad2 100644 --- a/src/Symfony/Component/PasswordHasher/Tests/Hasher/MessageDigestPasswordHasherTest.php +++ b/src/Symfony/Component/PasswordHasher/Tests/Hasher/MessageDigestPasswordHasherTest.php @@ -38,16 +38,19 @@ public function testHash() public function testHashAlgorithmDoesNotExist() { - $this->expectException(\LogicException::class); $hasher = new MessageDigestPasswordHasher('foobar'); + + $this->expectException(\LogicException::class); + $hasher->hash('password', ''); } public function testHashLength() { - $this->expectException(InvalidPasswordException::class); $hasher = new MessageDigestPasswordHasher(); + $this->expectException(InvalidPasswordException::class); + $hasher->hash(str_repeat('a', 5000), 'salt'); } diff --git a/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php b/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php index be60a3e163c8b..e2c2b8305fc22 100644 --- a/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php +++ b/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php @@ -130,7 +130,6 @@ public function testGetNullNamedHasherForHasherAware() public function testGetInvalidNamedHasherForHasherAware() { - $this->expectException(\RuntimeException::class); $factory = new PasswordHasherFactory([ HasherAwareUser::class => new MessageDigestPasswordHasher('sha1'), 'hasher_name' => new MessageDigestPasswordHasher('sha256'), @@ -138,6 +137,9 @@ public function testGetInvalidNamedHasherForHasherAware() $user = new HasherAwareUser(); $user->hasherName = 'invalid_hasher_name'; + + $this->expectException(\RuntimeException::class); + $factory->getPasswordHasher($user); } diff --git a/src/Symfony/Component/PasswordHasher/Tests/Hasher/Pbkdf2PasswordHasherTest.php b/src/Symfony/Component/PasswordHasher/Tests/Hasher/Pbkdf2PasswordHasherTest.php index 05785b2141c46..76279b3589eef 100644 --- a/src/Symfony/Component/PasswordHasher/Tests/Hasher/Pbkdf2PasswordHasherTest.php +++ b/src/Symfony/Component/PasswordHasher/Tests/Hasher/Pbkdf2PasswordHasherTest.php @@ -38,16 +38,19 @@ public function testHash() public function testHashAlgorithmDoesNotExist() { - $this->expectException(\LogicException::class); $hasher = new Pbkdf2PasswordHasher('foobar'); + + $this->expectException(\LogicException::class); + $hasher->hash('password', ''); } public function testHashLength() { - $this->expectException(InvalidPasswordException::class); $hasher = new Pbkdf2PasswordHasher('foobar'); + $this->expectException(InvalidPasswordException::class); + $hasher->hash(str_repeat('a', 5000), 'salt'); } diff --git a/src/Symfony/Component/PasswordHasher/Tests/Hasher/SodiumPasswordHasherTest.php b/src/Symfony/Component/PasswordHasher/Tests/Hasher/SodiumPasswordHasherTest.php index 3dc97c768f6f1..302b479be0c75 100644 --- a/src/Symfony/Component/PasswordHasher/Tests/Hasher/SodiumPasswordHasherTest.php +++ b/src/Symfony/Component/PasswordHasher/Tests/Hasher/SodiumPasswordHasherTest.php @@ -52,8 +52,8 @@ public function testNonArgonValidation() public function testHashLength() { $this->expectException(InvalidPasswordException::class); - $hasher = new SodiumPasswordHasher(); - $hasher->hash(str_repeat('a', 4097)); + + (new SodiumPasswordHasher())->hash(str_repeat('a', 4097)); } public function testCheckPasswordLength() diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index dfb4fd2936959..63ad90d95356a 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -328,11 +328,13 @@ public function testSetInputWhileRunningThrowsAnException() /** * @dataProvider provideInvalidInputValues */ - public function testInvalidInput($value) + public function testInvalidInput(array|object $value) { + $process = $this->getProcess('foo'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('"Symfony\Component\Process\Process::setInput" only accepts strings, Traversable objects or stream resources.'); - $process = $this->getProcess('foo'); + $process->setInput($value); } @@ -347,7 +349,7 @@ public static function provideInvalidInputValues() /** * @dataProvider provideInputValues */ - public function testValidInput($expected, $value) + public function testValidInput(?string $expected, null|float|string $value) { $process = $this->getProcess('foo'); $process->setInput($value); @@ -593,8 +595,10 @@ public function testSuccessfulMustRunHasCorrectExitCode() public function testMustRunThrowsException() { - $this->expectException(ProcessFailedException::class); $process = $this->getProcess('exit 1'); + + $this->expectException(ProcessFailedException::class); + $process->mustRun(); } @@ -972,9 +976,11 @@ public function testExitCodeIsAvailableAfterSignal() public function testSignalProcessNotRunning() { + $process = $this->getProcess('foo'); + $this->expectException(LogicException::class); $this->expectExceptionMessage('Cannot send signal on a non running process.'); - $process = $this->getProcess('foo'); + $process->signal(1); // SIGHUP } @@ -1062,20 +1068,24 @@ public function testDisableOutputDisablesTheOutput() public function testDisableOutputWhileRunningThrowsException() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Disabling output while the process is running is not possible.'); $p = $this->getProcessForCode('sleep(39);'); $p->start(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Disabling output while the process is running is not possible.'); + $p->disableOutput(); } public function testEnableOutputWhileRunningThrowsException() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Enabling output while the process is running is not possible.'); $p = $this->getProcessForCode('sleep(40);'); $p->disableOutput(); $p->start(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Enabling output while the process is running is not possible.'); + $p->enableOutput(); } @@ -1091,19 +1101,23 @@ public function testEnableOrDisableOutputAfterRunDoesNotThrowException() public function testDisableOutputWhileIdleTimeoutIsSet() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Output cannot be disabled while an idle timeout is set.'); $process = $this->getProcess('foo'); $process->setIdleTimeout(1); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Output cannot be disabled while an idle timeout is set.'); + $process->disableOutput(); } public function testSetIdleTimeoutWhileOutputIsDisabled() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('timeout cannot be set while the output is disabled.'); $process = $this->getProcess('foo'); $process->disableOutput(); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('timeout cannot be set while the output is disabled.'); + $process->setIdleTimeout(1); } @@ -1119,11 +1133,13 @@ public function testSetNullIdleTimeoutWhileOutputIsDisabled() */ public function testGetOutputWhileDisabled($fetchMethod) { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Output has been disabled.'); $p = $this->getProcessForCode('sleep(41);'); $p->disableOutput(); $p->start(); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Output has been disabled.'); + $p->{$fetchMethod}(); } @@ -1523,17 +1539,21 @@ public function testPreparedCommandWithQuoteInIt() public function testPreparedCommandWithMissingValue() { + $p = Process::fromShellCommandline('echo "${:abc}"'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Command line is missing a value for parameter "abc": echo "${:abc}"'); - $p = Process::fromShellCommandline('echo "${:abc}"'); + $p->run(null, ['bcd' => 'BCD']); } public function testPreparedCommandWithNoValues() { + $p = Process::fromShellCommandline('echo "${:abc}"'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Command line is missing a value for parameter "abc": echo "${:abc}"'); - $p = Process::fromShellCommandline('echo "${:abc}"'); + $p->run(null, []); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayAccessTestCase.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayAccessTestCase.php index 90d931873e667..9cc79f2c68184 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayAccessTestCase.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayAccessTestCase.php @@ -53,13 +53,14 @@ public function testGetValue($collection, $path, $value) public function testGetValueFailsIfNoSuchIndex() { - $this->expectException(NoSuchIndexException::class); $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() ->enableExceptionOnInvalidIndex() ->getPropertyAccessor(); $object = static::getContainer(['firstName' => 'Bernhard']); + $this->expectException(NoSuchIndexException::class); + $this->propertyAccessor->getValue($object, '[lastName]'); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTestCase.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTestCase.php index 742889ade2e01..f97260363c012 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTestCase.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTestCase.php @@ -156,8 +156,6 @@ public function testSetValueCallsAdderAndRemoverForNestedCollections() public function testSetValueFailsIfNoAdderNorRemoverFound() { - $this->expectException(NoSuchPropertyException::class); - $this->expectExceptionMessageMatches('/Could not determine access type for property "axes" in class "Mock_PropertyAccessorCollectionTestCase_CarNoAdderAndRemover_[^"]*"./'); $car = $this->createMock(__CLASS__.'_CarNoAdderAndRemover'); $axesBefore = $this->getContainer([1 => 'second', 3 => 'fourth']); $axesAfter = $this->getContainer([0 => 'first', 1 => 'second', 2 => 'third']); @@ -166,6 +164,9 @@ public function testSetValueFailsIfNoAdderNorRemoverFound() ->method('getAxes') ->willReturn($axesBefore); + $this->expectException(NoSuchPropertyException::class); + $this->expectExceptionMessageMatches('/Could not determine access type for property "axes" in class "Mock_PropertyAccessorCollectionTestCase_CarNoAdderAndRemover_[^"]*"./'); + $this->propertyAccessor->setValue($car, 'axes', $axesAfter); } @@ -195,9 +196,10 @@ public function testIsWritableReturnsFalseIfNoAdderNorRemoverExists() public function testSetValueFailsIfAdderAndRemoverExistButValueIsNotTraversable() { + $car = new PropertyAccessorCollectionTestCase_Car(); + $this->expectException(NoSuchPropertyException::class); $this->expectExceptionMessageMatches('/The property "axes" in class "Symfony\\\Component\\\PropertyAccess\\\Tests\\\PropertyAccessorCollectionTestCase_Car" can be defined with the methods "addAxis\(\)", "removeAxis\(\)" but the new value must be an array or an instance of \\\Traversable\./'); - $car = new PropertyAccessorCollectionTestCase_Car(); $this->propertyAccessor->setValue($car, 'axes', 'Not an array or Traversable'); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 7b4dfba0759f5..73c55be83133f 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -83,7 +83,7 @@ public static function getPathsWithMissingIndex() /** * @dataProvider getValidReadPropertyPaths */ - public function testGetValue($objectOrArray, $path, $value) + public function testGetValue(array|object $objectOrArray, string $path, ?string $value) { $this->assertSame($value, $this->propertyAccessor->getValue($objectOrArray, $path)); } @@ -91,7 +91,7 @@ public function testGetValue($objectOrArray, $path, $value) /** * @dataProvider getPathsWithMissingProperty */ - public function testGetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path) + public function testGetValueThrowsExceptionIfPropertyNotFound(array|object $objectOrArray, string $path) { $this->expectException(NoSuchPropertyException::class); $this->propertyAccessor->getValue($objectOrArray, $path); @@ -100,7 +100,7 @@ public function testGetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $p /** * @dataProvider getPathsWithMissingProperty */ - public function testGetValueReturnsNullIfPropertyNotFoundAndExceptionIsDisabled($objectOrArray, $path) + public function testGetValueReturnsNullIfPropertyNotFoundAndExceptionIsDisabled(array|object $objectOrArray, string $path) { $this->propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_SET, PropertyAccessor::DO_NOT_THROW); @@ -110,7 +110,7 @@ public function testGetValueReturnsNullIfPropertyNotFoundAndExceptionIsDisabled( /** * @dataProvider getPathsWithMissingIndex */ - public function testGetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path) + public function testGetValueThrowsNoExceptionIfIndexNotFound(array|object $objectOrArray, string $path) { $this->assertNull($this->propertyAccessor->getValue($objectOrArray, $path)); } @@ -118,10 +118,12 @@ public function testGetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $pa /** * @dataProvider getPathsWithMissingIndex */ - public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) + public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnabled(array|object $objectOrArray, string $path) { - $this->expectException(NoSuchIndexException::class); $this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_INDEX | PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH); + + $this->expectException(NoSuchIndexException::class); + $this->propertyAccessor->getValue($objectOrArray, $path); } @@ -143,9 +145,6 @@ public function testGetValueThrowsExceptionIfUninitializedPropertyWithGetter() public function testGetValueThrowsExceptionIfUninitializedPropertyWithGetterOfAnonymousClass() { - $this->expectException(UninitializedPropertyException::class); - $this->expectExceptionMessage('The method "class@anonymous::getUninitialized()" returned "null", but expected type "array". Did you forget to initialize a property or to make the return type nullable using "?array"?'); - $object = new class() { private $uninitialized; @@ -155,14 +154,14 @@ public function getUninitialized(): array } }; + $this->expectException(UninitializedPropertyException::class); + $this->expectExceptionMessage('The method "class@anonymous::getUninitialized()" returned "null", but expected type "array". Did you forget to initialize a property or to make the return type nullable using "?array"?'); + $this->propertyAccessor->getValue($object, 'uninitialized'); } public function testGetValueThrowsExceptionIfUninitializedNotNullablePropertyWithGetterOfAnonymousClass() { - $this->expectException(UninitializedPropertyException::class); - $this->expectExceptionMessage('The property "class@anonymous::$uninitialized" is not readable because it is typed "string". You should initialize it or declare a default value instead.'); - $object = new class() { private string $uninitialized; @@ -172,18 +171,21 @@ public function getUninitialized(): string } }; + $this->expectException(UninitializedPropertyException::class); + $this->expectExceptionMessage('The property "class@anonymous::$uninitialized" is not readable because it is typed "string". You should initialize it or declare a default value instead.'); + $this->propertyAccessor->getValue($object, 'uninitialized'); } public function testGetValueThrowsExceptionIfUninitializedPropertyOfAnonymousClass() { - $this->expectException(UninitializedPropertyException::class); - $this->expectExceptionMessage('The property "class@anonymous::$uninitialized" is not readable because it is typed "string". You should initialize it or declare a default value instead.'); - $object = new class() { public string $uninitialized; }; + $this->expectException(UninitializedPropertyException::class); + $this->expectExceptionMessage('The property "class@anonymous::$uninitialized" is not readable because it is typed "string". You should initialize it or declare a default value instead.'); + $this->propertyAccessor->getValue($object, 'uninitialized'); } @@ -205,9 +207,6 @@ public function testGetValueThrowsExceptionIfUninitializedNotNullablePropertyWit public function testGetValueThrowsExceptionIfUninitializedPropertyWithGetterOfAnonymousStdClass() { - $this->expectException(UninitializedPropertyException::class); - $this->expectExceptionMessage('The method "stdClass@anonymous::getUninitialized()" returned "null", but expected type "array". Did you forget to initialize a property or to make the return type nullable using "?array"?'); - $object = new class() extends \stdClass { private $uninitialized; @@ -217,16 +216,19 @@ public function getUninitialized(): array } }; + $this->expectException(UninitializedPropertyException::class); + $this->expectExceptionMessage('The method "stdClass@anonymous::getUninitialized()" returned "null", but expected type "array". Did you forget to initialize a property or to make the return type nullable using "?array"?'); + $this->propertyAccessor->getValue($object, 'uninitialized'); } public function testGetValueThrowsExceptionIfUninitializedPropertyWithGetterOfAnonymousChildClass() { + $object = new class() extends UninitializedPrivateProperty {}; + $this->expectException(UninitializedPropertyException::class); $this->expectExceptionMessage('The method "Symfony\Component\PropertyAccess\Tests\Fixtures\UninitializedPrivateProperty@anonymous::getUninitialized()" returned "null", but expected type "array". Did you forget to initialize a property or to make the return type nullable using "?array"?'); - $object = new class() extends \Symfony\Component\PropertyAccess\Tests\Fixtures\UninitializedPrivateProperty {}; - $this->propertyAccessor->getValue($object, 'uninitialized'); } @@ -312,7 +314,7 @@ public function testGetValueReadsMagicCallThatReturnsConstant() /** * @dataProvider getValidWritePropertyPaths */ - public function testSetValue($objectOrArray, $path) + public function testSetValue(array|object $objectOrArray, string $path) { $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); @@ -322,7 +324,7 @@ public function testSetValue($objectOrArray, $path) /** * @dataProvider getPathsWithMissingProperty */ - public function testSetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path) + public function testSetValueThrowsExceptionIfPropertyNotFound(array|object $objectOrArray, string $path) { $this->expectException(NoSuchPropertyException::class); $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); @@ -331,7 +333,7 @@ public function testSetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $p /** * @dataProvider getPathsWithMissingIndex */ - public function testSetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path) + public function testSetValueThrowsNoExceptionIfIndexNotFound(array|object $objectOrArray, string $path) { $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); @@ -341,7 +343,7 @@ public function testSetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $pa /** * @dataProvider getPathsWithMissingIndex */ - public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) + public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEnabled(array|object $objectOrArray, string $path) { $this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_INDEX | PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH); $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); @@ -351,9 +353,10 @@ public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEn public function testSetValueThrowsExceptionIfNotArrayAccess() { - $this->expectException(NoSuchIndexException::class); $object = new \stdClass(); + $this->expectException(NoSuchIndexException::class); + $this->propertyAccessor->setValue($object, '[index]', 'Updated'); } @@ -368,27 +371,30 @@ public function testSetValueUpdatesMagicSet() public function testSetValueIgnoresMagicSet() { - $this->expectException(NoSuchPropertyException::class); $propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS); $author = new TestClassMagicGet('Bernhard'); + $this->expectException(NoSuchPropertyException::class); + $propertyAccessor->setValue($author, 'magicProperty', 'Updated'); } public function testSetValueThrowsExceptionIfThereAreMissingParameters() { - $this->expectException(NoSuchPropertyException::class); $object = new TestClass('Bernhard'); + $this->expectException(NoSuchPropertyException::class); + $this->propertyAccessor->setValue($object, 'publicAccessorWithMoreRequiredParameters', 'Updated'); } public function testSetValueDoesNotUpdateMagicCallByDefault() { - $this->expectException(NoSuchPropertyException::class); $author = new TestClassMagicCall('Bernhard'); + $this->expectException(NoSuchPropertyException::class); + $this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated'); } @@ -412,7 +418,7 @@ public function testGetValueWhenArrayValueIsNull() /** * @dataProvider getValidReadPropertyPaths */ - public function testIsReadable($objectOrArray, $path) + public function testIsReadable(array|object $objectOrArray, string $path) { $this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path)); } @@ -420,7 +426,7 @@ public function testIsReadable($objectOrArray, $path) /** * @dataProvider getPathsWithMissingProperty */ - public function testIsReadableReturnsFalseIfPropertyNotFound($objectOrArray, $path) + public function testIsReadableReturnsFalseIfPropertyNotFound(array|object $objectOrArray, string $path) { $this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path)); } @@ -428,7 +434,7 @@ public function testIsReadableReturnsFalseIfPropertyNotFound($objectOrArray, $pa /** * @dataProvider getPathsWithMissingIndex */ - public function testIsReadableReturnsTrueIfIndexNotFound($objectOrArray, $path) + public function testIsReadableReturnsTrueIfIndexNotFound(array|object $objectOrArray, string $path) { // Non-existing indices can be read. In this case, null is returned $this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path)); @@ -437,7 +443,7 @@ public function testIsReadableReturnsTrueIfIndexNotFound($objectOrArray, $path) /** * @dataProvider getPathsWithMissingIndex */ - public function testIsReadableReturnsFalseIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) + public function testIsReadableReturnsFalseIfIndexNotFoundAndIndexExceptionsEnabled(array|object $objectOrArray, string $path) { $this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_INDEX | PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH); @@ -465,7 +471,7 @@ public function testIsReadableRecognizesMagicCallIfEnabled() /** * @dataProvider getValidWritePropertyPaths */ - public function testIsWritable($objectOrArray, $path) + public function testIsWritable(array|object $objectOrArray, string $path) { $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path)); } @@ -473,7 +479,7 @@ public function testIsWritable($objectOrArray, $path) /** * @dataProvider getPathsWithMissingProperty */ - public function testIsWritableReturnsFalseIfPropertyNotFound($objectOrArray, $path) + public function testIsWritableReturnsFalseIfPropertyNotFound(array|object $objectOrArray, string $path) { $this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path)); } @@ -481,7 +487,7 @@ public function testIsWritableReturnsFalseIfPropertyNotFound($objectOrArray, $pa /** * @dataProvider getPathsWithMissingIndex */ - public function testIsWritableReturnsTrueIfIndexNotFound($objectOrArray, $path) + public function testIsWritableReturnsTrueIfIndexNotFound(array|object $objectOrArray, string $path) { // Non-existing indices can be written. Arrays are created on-demand. $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path)); @@ -490,7 +496,7 @@ public function testIsWritableReturnsTrueIfIndexNotFound($objectOrArray, $path) /** * @dataProvider getPathsWithMissingIndex */ - public function testIsWritableReturnsTrueIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) + public function testIsWritableReturnsTrueIfIndexNotFoundAndIndexExceptionsEnabled(array|object $objectOrArray, string $path) { $this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_INDEX | PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH); @@ -588,7 +594,7 @@ public static function getNullSafeIndexPaths(): iterable /** * @dataProvider getNullSafeIndexPaths */ - public function testNullSafeIndexWithThrowOnInvalidIndex($objectOrArray, $path, $value) + public function testNullSafeIndexWithThrowOnInvalidIndex(array|object $objectOrArray, string $path, ?string $value) { $this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_INDEX | PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH); @@ -652,18 +658,20 @@ public function testIsWritableForReferenceChainIssue($object, $path, $value) public function testThrowTypeError() { + $object = new TypeHinted(); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Expected argument of type "DateTimeImmutable", "string" given at property path "date"'); - $object = new TypeHinted(); $this->propertyAccessor->setValue($object, 'date', 'This is a string, \DateTimeImmutable expected.'); } public function testThrowTypeErrorWithNullArgument() { + $object = new TypeHinted(); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Expected argument of type "DateTimeImmutable", "null" given'); - $object = new TypeHinted(); $this->propertyAccessor->setValue($object, 'date', null); } @@ -713,9 +721,10 @@ public function testAttributeWithSpecialChars() public function testThrowTypeErrorWithInterface() { + $object = new TypeHinted(); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Expected argument of type "Countable", "string" given'); - $object = new TypeHinted(); $this->propertyAccessor->setValue($object, 'countable', 'This is a string, \Countable expected.'); } @@ -733,9 +742,10 @@ public function testAnonymousClassRead() public function testAnonymousClassReadThrowExceptionOnInvalidPropertyPath() { - $this->expectException(NoSuchPropertyException::class); $obj = $this->generateAnonymousClass('bar'); + $this->expectException(NoSuchPropertyException::class); + $this->propertyAccessor->getValue($obj, 'invalid_property'); } @@ -784,25 +794,28 @@ public function setFoo($foo) public function testThrowTypeErrorInsideSetterCall() { - $this->expectException(\TypeError::class); $object = new TestClassTypeErrorInsideCall(); + $this->expectException(\TypeError::class); + $this->propertyAccessor->setValue($object, 'property', 'foo'); } public function testDoNotDiscardReturnTypeError() { - $this->expectException(\TypeError::class); $object = new ReturnTyped(); + $this->expectException(\TypeError::class); + $this->propertyAccessor->setValue($object, 'foos', [new \DateTimeImmutable()]); } public function testDoNotDiscardReturnTypeErrorWhenWriterMethodIsMisconfigured() { - $this->expectException(\TypeError::class); $object = new ReturnTyped(); + $this->expectException(\TypeError::class); + $this->propertyAccessor->setValue($object, 'name', 'foo'); } @@ -850,41 +863,51 @@ public function testAdderAndRemoverArePreferredOverSetterForSameSingularAndPlura public function testAdderWithoutRemover() { + $object = new TestAdderRemoverInvalidMethods(); + $this->expectException(NoSuchPropertyException::class); $this->expectExceptionMessageMatches('/.*The add method "addFoo" in class "Symfony\\\Component\\\PropertyAccess\\\Tests\\\Fixtures\\\TestAdderRemoverInvalidMethods" was found, but the corresponding remove method "removeFoo" was not found\./'); - $object = new TestAdderRemoverInvalidMethods(); + $this->propertyAccessor->setValue($object, 'foos', [1, 2]); } public function testRemoverWithoutAdder() { + $object = new TestAdderRemoverInvalidMethods(); + $this->expectException(NoSuchPropertyException::class); $this->expectExceptionMessageMatches('/.*The remove method "removeBar" in class "Symfony\\\Component\\\PropertyAccess\\\Tests\\\Fixtures\\\TestAdderRemoverInvalidMethods" was found, but the corresponding add method "addBar" was not found\./'); - $object = new TestAdderRemoverInvalidMethods(); + $this->propertyAccessor->setValue($object, 'bars', [1, 2]); } public function testAdderAndRemoveNeedsTheExactParametersDefined() { + $object = new TestAdderRemoverInvalidArgumentLength(); + $this->expectException(NoSuchPropertyException::class); $this->expectExceptionMessageMatches('/.*The method "addFoo" in class "Symfony\\\Component\\\PropertyAccess\\\Tests\\\Fixtures\\\TestAdderRemoverInvalidArgumentLength" requires 0 arguments, but should accept only 1\./'); - $object = new TestAdderRemoverInvalidArgumentLength(); + $this->propertyAccessor->setValue($object, 'foo', [1, 2]); } public function testSetterNeedsTheExactParametersDefined() { + $object = new TestAdderRemoverInvalidArgumentLength(); + $this->expectException(NoSuchPropertyException::class); $this->expectExceptionMessageMatches('/.*The method "setBar" in class "Symfony\\\Component\\\PropertyAccess\\\Tests\\\Fixtures\\\TestAdderRemoverInvalidArgumentLength" requires 2 arguments, but should accept only 1\./'); - $object = new TestAdderRemoverInvalidArgumentLength(); + $this->propertyAccessor->setValue($object, 'bar', [1, 2]); } public function testSetterNeedsPublicAccess() { + $object = new TestClassSetValue(0); + $this->expectException(NoSuchPropertyException::class); $this->expectExceptionMessageMatches('/.*The method "setFoo" in class "Symfony\\\Component\\\PropertyAccess\\\Tests\\\Fixtures\\\TestClassSetValue" was found but does not have public access./'); - $object = new TestClassSetValue(0); + $this->propertyAccessor->setValue($object, 'foo', 1); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyPathBuilderTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathBuilderTest.php index 948ca066cb8fb..fe21325b3d1af 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyPathBuilderTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathBuilderTest.php @@ -185,7 +185,7 @@ public function testReplaceNegative() /** * @dataProvider provideInvalidOffsets */ - public function testReplaceDoesNotAllowInvalidOffsets($offset) + public function testReplaceDoesNotAllowInvalidOffsets(int $offset) { $this->expectException(\OutOfBoundsException::class); $this->builder->replace($offset, 1, new PropertyPath('new1[new2].new3')); diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php index 40a9346088fda..9257229c3aebf 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php @@ -52,7 +52,7 @@ public static function providePathsContainingUnexpectedCharacters() /** * @dataProvider providePathsContainingUnexpectedCharacters */ - public function testUnexpectedCharacters($path) + public function testUnexpectedCharacters(string $path) { $this->expectException(InvalidPropertyPathException::class); new PropertyPath($path); @@ -137,17 +137,19 @@ public function testGetElement() public function testGetElementDoesNotAcceptInvalidIndices() { - $this->expectException(\OutOfBoundsException::class); $propertyPath = new PropertyPath('grandpa.parent[child]'); + $this->expectException(\OutOfBoundsException::class); + $propertyPath->getElement(3); } public function testGetElementDoesNotAcceptNegativeIndices() { - $this->expectException(\OutOfBoundsException::class); $propertyPath = new PropertyPath('grandpa.parent[child]'); + $this->expectException(\OutOfBoundsException::class); + $propertyPath->getElement(-1); } @@ -161,17 +163,19 @@ public function testIsProperty() public function testIsPropertyDoesNotAcceptInvalidIndices() { - $this->expectException(\OutOfBoundsException::class); $propertyPath = new PropertyPath('grandpa.parent[child]'); + $this->expectException(\OutOfBoundsException::class); + $propertyPath->isProperty(3); } public function testIsPropertyDoesNotAcceptNegativeIndices() { - $this->expectException(\OutOfBoundsException::class); $propertyPath = new PropertyPath('grandpa.parent[child]'); + $this->expectException(\OutOfBoundsException::class); + $propertyPath->isProperty(-1); } @@ -185,17 +189,19 @@ public function testIsIndex() public function testIsIndexDoesNotAcceptInvalidIndices() { - $this->expectException(\OutOfBoundsException::class); $propertyPath = new PropertyPath('grandpa.parent[child]'); + $this->expectException(\OutOfBoundsException::class); + $propertyPath->isIndex(3); } public function testIsIndexDoesNotAcceptNegativeIndices() { - $this->expectException(\OutOfBoundsException::class); $propertyPath = new PropertyPath('grandpa.parent[child]'); + $this->expectException(\OutOfBoundsException::class); + $propertyPath->isIndex(-1); } } diff --git a/src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php index 2a79293a0ece9..6662cba70f3dd 100644 --- a/src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php +++ b/src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php @@ -49,23 +49,25 @@ public function testReserve() public function testReserveMoreTokensThanBucketSize() { + $limiter = $this->createLimiter(); + $this->expectException(\LogicException::class); $this->expectExceptionMessage('Cannot reserve more tokens (15) than the burst size of the rate limiter (10).'); - $limiter = $this->createLimiter(); $limiter->reserve(15); } public function testReserveMaxWaitingTime() { - $this->expectException(MaxWaitDurationExceededException::class); - $limiter = $this->createLimiter(10, Rate::perMinute()); // enough free tokens $this->assertEquals(0, $limiter->reserve(10, 300)->getWaitDuration()); // waiting time within set maximum $this->assertEquals(300, $limiter->reserve(5, 300)->getWaitDuration()); + + $this->expectException(MaxWaitDurationExceededException::class); + // waiting time exceeded maximum time (as 5 tokens are already reserved) $limiter->reserve(5, 300); } diff --git a/src/Symfony/Component/RateLimiter/Tests/RateLimiterFactoryTest.php b/src/Symfony/Component/RateLimiter/Tests/RateLimiterFactoryTest.php index 5ac5963a2a1cb..c60ff6f0c53fd 100644 --- a/src/Symfony/Component/RateLimiter/Tests/RateLimiterFactoryTest.php +++ b/src/Symfony/Component/RateLimiter/Tests/RateLimiterFactoryTest.php @@ -66,8 +66,8 @@ public static function validConfigProvider() public function testInvalidConfig(string $exceptionClass, array $config) { $this->expectException($exceptionClass); + $factory = new RateLimiterFactory($config, new InMemoryStorage()); - $factory->create('key'); } public static function invalidConfigProvider() diff --git a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php index 2c599730f0b09..4db2f9596764b 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php @@ -86,8 +86,10 @@ public function testRelativeUrlWithNullParameter() public function testRelativeUrlWithNullParameterButNotOptional() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/testing/{foo}/bar', ['foo' => null])); + + $this->expectException(InvalidParameterException::class); + // This must raise an exception because the default requirement for "foo" is "[^/]+" which is not met with these params. // Generating path "/testing//bar" would be wrong as matching this route would fail. $this->getGenerator($routes)->generate('test', [], UrlGeneratorInterface::ABSOLUTE_PATH); @@ -294,18 +296,17 @@ public function testDumpWithLocalizedRoutesPreserveTheGoodLocaleInTheUrl() public function testGenerateWithoutRoutes() { - $this->expectException(RouteNotFoundException::class); $routes = $this->getRoutes('foo', new Route('/testing/{foo}')); + + $this->expectException(RouteNotFoundException::class); + $this->getGenerator($routes)->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL); } public function testGenerateWithInvalidLocale() { - $this->expectException(RouteNotFoundException::class); $routes = new RouteCollection(); - $route = new Route(''); - $name = 'test'; foreach (['hr' => '/foo', 'en' => '/bar'] as $locale => $path) { @@ -318,28 +319,37 @@ public function testGenerateWithInvalidLocale() } $generator = $this->getGenerator($routes, [], null, 'fr'); + + $this->expectException(RouteNotFoundException::class); + $generator->generate($name); } public function testGenerateForRouteWithoutMandatoryParameter() { + $routes = $this->getRoutes('test', new Route('/testing/{foo}')); + $this->expectException(MissingMandatoryParametersException::class); $this->expectExceptionMessage('Some mandatory parameters are missing ("foo") to generate a URL for route "test".'); - $routes = $this->getRoutes('test', new Route('/testing/{foo}')); + $this->getGenerator($routes)->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL); } public function testGenerateForRouteWithInvalidOptionalParameter() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/testing/{foo}', ['foo' => '1'], ['foo' => 'd+'])); + + $this->expectException(InvalidParameterException::class); + $this->getGenerator($routes)->generate('test', ['foo' => 'bar'], UrlGeneratorInterface::ABSOLUTE_URL); } public function testGenerateForRouteWithInvalidParameter() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/testing/{foo}', [], ['foo' => '1|2'])); + + $this->expectException(InvalidParameterException::class); + $this->getGenerator($routes)->generate('test', ['foo' => '0'], UrlGeneratorInterface::ABSOLUTE_URL); } @@ -372,22 +382,28 @@ public function testGenerateForRouteWithInvalidParameterButDisabledRequirementsC public function testGenerateForRouteWithInvalidMandatoryParameter() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/testing/{foo}', [], ['foo' => 'd+'])); + + $this->expectException(InvalidParameterException::class); + $this->getGenerator($routes)->generate('test', ['foo' => 'bar'], UrlGeneratorInterface::ABSOLUTE_URL); } public function testGenerateForRouteWithInvalidUtf8Parameter() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/testing/{foo}', [], ['foo' => '\pL+'], ['utf8' => true])); + + $this->expectException(InvalidParameterException::class); + $this->getGenerator($routes)->generate('test', ['foo' => 'abc123'], UrlGeneratorInterface::ABSOLUTE_URL); } public function testRequiredParamAndEmptyPassed() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/{slug}', [], ['slug' => '.+'])); + + $this->expectException(InvalidParameterException::class); + $this->getGenerator($routes)->generate('test', ['slug' => '']); } @@ -561,25 +577,30 @@ public function testImportantVariable() public function testImportantVariableWithNoDefault() { - $this->expectException(MissingMandatoryParametersException::class); - $this->expectExceptionMessage('Some mandatory parameters are missing ("_format") to generate a URL for route "test".'); $routes = $this->getRoutes('test', new Route('/{page}.{!_format}')); $generator = $this->getGenerator($routes); + $this->expectException(MissingMandatoryParametersException::class); + $this->expectExceptionMessage('Some mandatory parameters are missing ("_format") to generate a URL for route "test".'); + $generator->generate('test', ['page' => 'index']); } public function testDefaultRequirementOfVariableDisallowsSlash() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/{page}.{_format}')); + + $this->expectException(InvalidParameterException::class); + $this->getGenerator($routes)->generate('test', ['page' => 'index', '_format' => 'sl/ash']); } public function testDefaultRequirementOfVariableDisallowsNextSeparator() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/{page}.{_format}')); + + $this->expectException(InvalidParameterException::class); + $this->getGenerator($routes)->generate('test', ['page' => 'do.t', '_format' => 'html']); } @@ -606,22 +627,28 @@ public function testWithHostSameAsContextAndAbsolute() public function testUrlWithInvalidParameterInHost() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/', [], ['foo' => 'bar'], [], '{foo}.example.com')); + + $this->expectException(InvalidParameterException::class); + $this->getGenerator($routes)->generate('test', ['foo' => 'baz'], UrlGeneratorInterface::ABSOLUTE_PATH); } public function testUrlWithInvalidParameterInHostWhenParamHasADefaultValue() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/', ['foo' => 'bar'], ['foo' => 'bar'], [], '{foo}.example.com')); + + $this->expectException(InvalidParameterException::class); + $this->getGenerator($routes)->generate('test', ['foo' => 'baz'], UrlGeneratorInterface::ABSOLUTE_PATH); } public function testUrlWithInvalidParameterEqualsDefaultValueInHost() { - $this->expectException(InvalidParameterException::class); $routes = $this->getRoutes('test', new Route('/', ['foo' => 'baz'], ['foo' => 'bar'], [], '{foo}.example.com')); + + $this->expectException(InvalidParameterException::class); + $this->getGenerator($routes)->generate('test', ['foo' => 'baz'], UrlGeneratorInterface::ABSOLUTE_PATH); } @@ -771,11 +798,11 @@ public function testAliases() public function testAliasWhichTargetRouteDoesntExist() { - $this->expectException(RouteNotFoundException::class); - $routes = new RouteCollection(); $routes->addAlias('d', 'non-existent'); + $this->expectException(RouteNotFoundException::class); + $this->getGenerator($routes)->generate('d'); } @@ -827,39 +854,39 @@ public function testTargettingADeprecatedAliasShouldTriggerDeprecation() public function testCircularReferenceShouldThrowAnException() { - $this->expectException(RouteCircularReferenceException::class); - $this->expectExceptionMessage('Circular reference detected for route "b", path: "b -> a -> b".'); - $routes = new RouteCollection(); $routes->addAlias('a', 'b'); $routes->addAlias('b', 'a'); + $this->expectException(RouteCircularReferenceException::class); + $this->expectExceptionMessage('Circular reference detected for route "b", path: "b -> a -> b".'); + $this->getGenerator($routes)->generate('b'); } public function testDeepCircularReferenceShouldThrowAnException() { - $this->expectException(RouteCircularReferenceException::class); - $this->expectExceptionMessage('Circular reference detected for route "b", path: "b -> c -> b".'); - $routes = new RouteCollection(); $routes->addAlias('a', 'b'); $routes->addAlias('b', 'c'); $routes->addAlias('c', 'b'); + $this->expectException(RouteCircularReferenceException::class); + $this->expectExceptionMessage('Circular reference detected for route "b", path: "b -> c -> b".'); + $this->getGenerator($routes)->generate('b'); } public function testIndirectCircularReferenceShouldThrowAnException() { - $this->expectException(RouteCircularReferenceException::class); - $this->expectExceptionMessage('Circular reference detected for route "a", path: "a -> b -> c -> a".'); - $routes = new RouteCollection(); $routes->addAlias('a', 'b'); $routes->addAlias('b', 'c'); $routes->addAlias('c', 'a'); + $this->expectException(RouteCircularReferenceException::class); + $this->expectExceptionMessage('Circular reference detected for route "a", path: "a -> b -> c -> a".'); + $this->getGenerator($routes)->generate('a'); } diff --git a/src/Symfony/Component/Routing/Tests/Loader/ObjectLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/ObjectLoaderTest.php index 54717b6116ae8..c5aeff9f7f658 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/ObjectLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/ObjectLoaderTest.php @@ -45,8 +45,10 @@ public function testLoadCallsServiceAndReturnsCollection() */ public function testExceptionWithoutSyntax(string $resourceString) { - $this->expectException(\InvalidArgumentException::class); $loader = new TestObjectLoader(); + + $this->expectException(\InvalidArgumentException::class); + $loader->load($resourceString); } @@ -64,23 +66,26 @@ public static function getBadResourceStrings() public function testExceptionOnNoObjectReturned() { - $this->expectException(\TypeError::class); $loader = new TestObjectLoader(); $loader->loaderMap = ['my_service' => 'NOT_AN_OBJECT']; + + $this->expectException(\TypeError::class); + $loader->load('my_service::method'); } public function testExceptionOnBadMethod() { - $this->expectException(\BadMethodCallException::class); $loader = new TestObjectLoader(); $loader->loaderMap = ['my_service' => new \stdClass()]; + + $this->expectException(\BadMethodCallException::class); + $loader->load('my_service::method'); } public function testExceptionOnMethodNotReturningCollection() { - $this->expectException(\LogicException::class); $service = $this->getMockBuilder(\stdClass::class) ->addMethods(['loadRoutes']) ->getMock(); @@ -90,6 +95,9 @@ public function testExceptionOnMethodNotReturningCollection() $loader = new TestObjectLoader(); $loader->loaderMap = ['my_service' => $service]; + + $this->expectException(\LogicException::class); + $loader->load('my_service::loadRoutes'); } } @@ -105,7 +113,7 @@ public function supports(mixed $resource, string $type = null): bool protected function getObject(string $id): object { - return $this->loaderMap[$id] ?? null; + return $this->loaderMap[$id]; } } diff --git a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php index 9e42db7a7e6fe..5291535fd1f72 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php @@ -219,18 +219,22 @@ public function testLocalizedImportsOfNotLocalizedRoutes() */ public function testLoadThrowsExceptionWithInvalidFile($filePath) { - $this->expectException(\InvalidArgumentException::class); $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); + + $this->expectException(\InvalidArgumentException::class); + $loader->load($filePath); } /** * @dataProvider getPathsToInvalidFiles */ - public function testLoadThrowsExceptionWithInvalidFileEvenWithoutSchemaValidation($filePath) + public function testLoadThrowsExceptionWithInvalidFileEvenWithoutSchemaValidation(string $filePath) { - $this->expectException(\InvalidArgumentException::class); $loader = new CustomXmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); + + $this->expectException(\InvalidArgumentException::class); + $loader->load($filePath); } @@ -250,9 +254,11 @@ public static function getPathsToInvalidFiles() public function testDocTypeIsNotAllowed() { + $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Document types are not allowed.'); - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); + $loader->load('withdoctype.xml'); } @@ -458,16 +464,18 @@ public function testLoadRouteWithControllerSetInDefaults() public function testOverrideControllerInDefaults() { + $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessageMatches('/The routing file "[^"]*" must not specify both the "controller" attribute and the defaults key "_controller" for "app_blog"/'); - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); + $loader->load('override_defaults.xml'); } /** * @dataProvider provideFilesImportingRoutesWithControllers */ - public function testImportRouteWithController($file) + public function testImportRouteWithController(string $file) { $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); $routeCollection = $loader->load($file); @@ -490,9 +498,11 @@ public static function provideFilesImportingRoutesWithControllers() public function testImportWithOverriddenController() { + $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessageMatches('/The routing file "[^"]*" must not specify both the "controller" attribute and the defaults key "_controller" for the "import" tag/'); - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); + $loader->load('import_override_defaults.xml'); } diff --git a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php index c925affcf1c7c..5e19254d8737a 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php @@ -49,10 +49,12 @@ public function testLoadDoesNothingIfEmpty() /** * @dataProvider getPathsToInvalidFiles */ - public function testLoadThrowsExceptionWithInvalidFile($filePath) + public function testLoadThrowsExceptionWithInvalidFile(string $filePath) { - $this->expectException(\InvalidArgumentException::class); $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); + + $this->expectException(\InvalidArgumentException::class); + $loader->load($filePath); } @@ -151,9 +153,11 @@ public function testLoadRouteWithControllerSetInDefaults() public function testOverrideControllerInDefaults() { + $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessageMatches('/The routing file "[^"]*" must not specify both the "controller" key and the defaults key "_controller" for "app_blog"/'); - $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); + $loader->load('override_defaults.yml'); } @@ -183,9 +187,11 @@ public static function provideFilesImportingRoutesWithControllers() public function testImportWithOverriddenController() { + $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessageMatches('/The routing file "[^"]*" must not specify both the "controller" key and the defaults key "_controller" for "_static"/'); - $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); + $loader->load('import_override_defaults.yml'); } @@ -396,10 +402,11 @@ public function testImportRouteWithNoTrailingSlash() public function testRequirementsWithoutPlaceholderName() { + $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('A placeholder name must be a string (0 given). Did you forget to specify the placeholder key for the requirement "\\d+" of route "foo"'); - $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); $loader->load('requirements_without_placeholder_name.yml'); } diff --git a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php index 232314b5ab734..d61d736ad0ebb 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php @@ -493,11 +493,13 @@ private function generateDumpedMatcher(RouteCollection $collection) public function testGenerateDumperMatcherWithObject() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Symfony\Component\Routing\Route cannot contain objects'); $routeCollection = new RouteCollection(); $routeCollection->add('_', new Route('/', [new \stdClass()])); $dumper = new CompiledUrlMatcherDumper($routeCollection); + + $this->expectExceptionMessage('Symfony\Component\Routing\Route cannot contain objects'); + $this->expectException(\InvalidArgumentException::class); + $dumper->dump(); } } diff --git a/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php index 1f3774b5b4e69..dc8126a43cb42 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php @@ -41,13 +41,15 @@ public function testExtraTrailingSlash() public function testRedirectWhenNoSlashForNonSafeMethod() { - $this->expectException(ResourceNotFoundException::class); $coll = new RouteCollection(); $coll->add('foo', new Route('/foo/')); $context = new RequestContext(); $context->setMethod('POST'); $matcher = $this->getUrlMatcher($coll, $context); + + $this->expectException(ResourceNotFoundException::class); + $matcher->match('/foo'); } diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index 41126642e4767..34966dfe82fb0 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -199,21 +199,25 @@ public function testMatchImportantVariable() public function testShortPathDoesNotMatchImportantVariable() { - $this->expectException(ResourceNotFoundException::class); - $collection = new RouteCollection(); $collection->add('index', new Route('/index.{!_format}', ['_format' => 'xml'])); - $this->getUrlMatcher($collection)->match('/index'); + $matcher = $this->getUrlMatcher($collection); + + $this->expectException(ResourceNotFoundException::class); + + $matcher->match('/index'); } public function testTrailingEncodedNewlineIsNotOverlooked() { - $this->expectException(ResourceNotFoundException::class); $collection = new RouteCollection(); $collection->add('foo', new Route('/foo')); $matcher = $this->getUrlMatcher($collection); + + $this->expectException(ResourceNotFoundException::class); + $matcher->match('/foo%0a'); } @@ -358,31 +362,35 @@ public function testDefaultRequirementOfVariable() public function testDefaultRequirementOfVariableDisallowsSlash() { - $this->expectException(ResourceNotFoundException::class); $coll = new RouteCollection(); $coll->add('test', new Route('/{page}.{_format}')); $matcher = $this->getUrlMatcher($coll); + $this->expectException(ResourceNotFoundException::class); + $matcher->match('/index.sl/ash'); } public function testDefaultRequirementOfVariableDisallowsNextSeparator() { - $this->expectException(ResourceNotFoundException::class); $coll = new RouteCollection(); $coll->add('test', new Route('/{page}.{_format}', [], ['_format' => 'html|xml'])); $matcher = $this->getUrlMatcher($coll); + $this->expectException(ResourceNotFoundException::class); + $matcher->match('/do.t.html'); } public function testMissingTrailingSlash() { - $this->expectException(ResourceNotFoundException::class); $coll = new RouteCollection(); $coll->add('foo', new Route('/foo/')); $matcher = $this->getUrlMatcher($coll); + + $this->expectException(ResourceNotFoundException::class); + $matcher->match('/foo'); } @@ -452,12 +460,14 @@ public function testSamePathWithDifferentScheme() public function testCondition() { - $this->expectException(ResourceNotFoundException::class); $coll = new RouteCollection(); $route = new Route('/foo'); $route->setCondition('context.getMethod() == "POST"'); $coll->add('foo', $route); $matcher = $this->getUrlMatcher($coll); + + $this->expectException(ResourceNotFoundException::class); + $matcher->match('/foo'); } @@ -690,21 +700,25 @@ public function testMixOfStaticAndVariableVariationInTrailingSlashWithMethods() public function testWithOutHostHostDoesNotMatch() { - $this->expectException(ResourceNotFoundException::class); $coll = new RouteCollection(); $coll->add('foo', new Route('/foo/{foo}', [], [], [], '{locale}.example.com')); $matcher = $this->getUrlMatcher($coll, new RequestContext('', 'GET', 'example.com')); + + $this->expectException(ResourceNotFoundException::class); + $matcher->match('/foo/bar'); } public function testPathIsCaseSensitive() { - $this->expectException(ResourceNotFoundException::class); $coll = new RouteCollection(); $coll->add('foo', new Route('/locale', [], ['locale' => 'EN|FR|DE'])); $matcher = $this->getUrlMatcher($coll); + + $this->expectException(ResourceNotFoundException::class); + $matcher->match('/en'); } @@ -719,10 +733,12 @@ public function testHostIsCaseInsensitive() public function testNoConfiguration() { - $this->expectException(NoConfigurationException::class); $coll = new RouteCollection(); $matcher = $this->getUrlMatcher($coll); + + $this->expectException(NoConfigurationException::class); + $matcher->match('/'); } @@ -752,12 +768,14 @@ public function testNestedCollections() public function testSchemeAndMethodMismatch() { - $this->expectException(ResourceNotFoundException::class); - $this->expectExceptionMessage('No routes found for "/".'); $coll = new RouteCollection(); $coll->add('foo', new Route('/', [], [], [], null, ['https'], ['POST'])); $matcher = $this->getUrlMatcher($coll); + + $this->expectException(ResourceNotFoundException::class); + $this->expectExceptionMessage('No routes found for "/".'); + $matcher->match('/'); } diff --git a/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php b/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php index 63186881afb33..b53c37f67c408 100644 --- a/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php @@ -252,32 +252,35 @@ public function testRouteWithSameVariableTwice() public function testRouteCharsetMismatch() { - $this->expectException(\LogicException::class); $route = new Route("/\xE9/{bar}", [], ['bar' => '.'], ['utf8' => true]); + $this->expectException(\LogicException::class); + $route->compile(); } public function testRequirementCharsetMismatch() { - $this->expectException(\LogicException::class); $route = new Route('/foo/{bar}', [], ['bar' => "\xE9"], ['utf8' => true]); + $this->expectException(\LogicException::class); + $route->compile(); } public function testRouteWithFragmentAsPathParameter() { - $this->expectException(\InvalidArgumentException::class); $route = new Route('/{_fragment}'); + $this->expectException(\InvalidArgumentException::class); + $route->compile(); } /** * @dataProvider getVariableNamesStartingWithADigit */ - public function testRouteWithVariableNameStartingWithADigit($name) + public function testRouteWithVariableNameStartingWithADigit(string $name) { $this->expectException(\DomainException::class); $route = new Route('/{'.$name.'}'); @@ -296,7 +299,7 @@ public static function getVariableNamesStartingWithADigit() /** * @dataProvider provideCompileWithHostData */ - public function testCompileWithHost($name, $arguments, $prefix, $regex, $variables, $pathVariables, $tokens, $hostRegex, $hostVariables, $hostTokens) + public function testCompileWithHost(string $name, array $arguments, string $prefix, string $regex, array $variables, array $pathVariables, array $tokens, string $hostRegex, array $hostVariables, array $hostTokens) { $r = new \ReflectionClass(Route::class); $route = $r->newInstanceArgs($arguments); @@ -366,15 +369,17 @@ public static function provideCompileWithHostData() public function testRouteWithTooLongVariableName() { - $this->expectException(\DomainException::class); $route = new Route(sprintf('/{%s}', str_repeat('a', RouteCompiler::VARIABLE_MAXIMUM_LENGTH + 1))); + + $this->expectException(\DomainException::class); + $route->compile(); } /** * @dataProvider provideRemoveCapturingGroup */ - public function testRemoveCapturingGroup($regex, $requirement) + public function testRemoveCapturingGroup(string $regex, string $requirement) { $route = new Route('/{foo}', [], ['foo' => $requirement]); diff --git a/src/Symfony/Component/Routing/Tests/RouteTest.php b/src/Symfony/Component/Routing/Tests/RouteTest.php index b68ddd0e7b245..176c6f05ee021 100644 --- a/src/Symfony/Component/Routing/Tests/RouteTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteTest.php @@ -146,8 +146,10 @@ public function testRequirementAlternativeStartAndEndRegexSyntax() */ public function testSetInvalidRequirement($req) { - $this->expectException(\InvalidArgumentException::class); $route = new Route('/{foo}'); + + $this->expectException(\InvalidArgumentException::class); + $route->setRequirement('foo', $req); } diff --git a/src/Symfony/Component/Routing/Tests/RouterTest.php b/src/Symfony/Component/Routing/Tests/RouterTest.php index b8766831bd580..fa8c66f2fad83 100644 --- a/src/Symfony/Component/Routing/Tests/RouterTest.php +++ b/src/Symfony/Component/Routing/Tests/RouterTest.php @@ -89,7 +89,7 @@ public function testGetOptionWithUnsupportedOption() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The Router does not support the "option_foo" option'); - $this->router->getOption('option_foo', true); + $this->router->getOption('option_foo'); } public function testThatRouteCollectionIsLoaded() diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/InMemoryTokenProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/InMemoryTokenProviderTest.php index 45fd046a9ac73..6fc2ab1555a1b 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/InMemoryTokenProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/InMemoryTokenProviderTest.php @@ -31,8 +31,7 @@ public function testCreateNewToken() public function testLoadTokenBySeriesThrowsNotFoundException() { $this->expectException(TokenNotFoundException::class); - $provider = new InMemoryTokenProvider(); - $provider->loadTokenBySeries('foo'); + (new InMemoryTokenProvider())->loadTokenBySeries('foo'); } public function testUpdateToken() @@ -50,12 +49,14 @@ public function testUpdateToken() public function testDeleteToken() { - $this->expectException(TokenNotFoundException::class); $provider = new InMemoryTokenProvider(); $token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTimeImmutable()); $provider->createNewToken($token); $provider->deleteTokenBySeries('foo'); + + $this->expectException(TokenNotFoundException::class); + $provider->loadTokenBySeries('foo'); } } diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php index 80c3f4a00b6a2..5636340e6aea2 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php @@ -67,8 +67,7 @@ public function testVoteWithTypeError() { $this->expectException(\TypeError::class); $this->expectExceptionMessage('Should error'); - $voter = new TypeErrorVoterTest_Voter(); - $voter->vote($this->token, new \stdClass(), ['EDIT']); + (new TypeErrorVoterTest_Voter())->vote($this->token, new \stdClass(), ['EDIT']); } } diff --git a/src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php b/src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php index 09227752bb0ee..901115615a3df 100644 --- a/src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php @@ -47,7 +47,6 @@ public function testLoadUserByIdentifier() public function testLoadUserByIdentifierThrowsUserNotFoundException() { - $this->expectException(UserNotFoundException::class); $provider1 = $this->createMock(InMemoryUserProvider::class); $provider1 ->expects($this->once()) @@ -65,6 +64,9 @@ public function testLoadUserByIdentifierThrowsUserNotFoundException() ; $provider = new ChainUserProvider([$provider1, $provider2]); + + $this->expectException(UserNotFoundException::class); + $provider->loadUserByIdentifier('foo'); } @@ -141,7 +143,6 @@ public function testRefreshUserAgain() public function testRefreshUserThrowsUnsupportedUserException() { - $this->expectException(UnsupportedUserException::class); $provider1 = $this->createMock(InMemoryUserProvider::class); $provider1 ->expects($this->once()) @@ -169,6 +170,9 @@ public function testRefreshUserThrowsUnsupportedUserException() ; $provider = new ChainUserProvider([$provider1, $provider2]); + + $this->expectException(UnsupportedUserException::class); + $provider->refreshUser($this->createMock(UserInterface::class)); } diff --git a/src/Symfony/Component/Security/Core/Tests/User/InMemoryUserCheckerTest.php b/src/Symfony/Component/Security/Core/Tests/User/InMemoryUserCheckerTest.php index 8b01e5f02e880..25107723e4fc7 100644 --- a/src/Symfony/Component/Security/Core/Tests/User/InMemoryUserCheckerTest.php +++ b/src/Symfony/Component/Security/Core/Tests/User/InMemoryUserCheckerTest.php @@ -35,7 +35,6 @@ public function testCheckPostAuthPass() public function testCheckPreAuthDisabled() { $this->expectException(DisabledException::class); - $checker = new InMemoryUserChecker(); - $checker->checkPreAuth(new InMemoryUser('John', 'password', [], false)); + (new InMemoryUserChecker())->checkPreAuth(new InMemoryUser('John', 'password', [], false)); } } diff --git a/src/Symfony/Component/Security/Core/Tests/User/InMemoryUserProviderTest.php b/src/Symfony/Component/Security/Core/Tests/User/InMemoryUserProviderTest.php index 1a843e4e71c55..98afb3b4f2230 100644 --- a/src/Symfony/Component/Security/Core/Tests/User/InMemoryUserProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/User/InMemoryUserProviderTest.php @@ -62,16 +62,17 @@ public function testCreateUser() public function testCreateUserAlreadyExist() { - $this->expectException(\LogicException::class); $provider = new InMemoryUserProvider(); $provider->createUser(new InMemoryUser('fabien', 'foo')); + + $this->expectException(\LogicException::class); + $provider->createUser(new InMemoryUser('fabien', 'foo')); } public function testLoadUserByIdentifierDoesNotExist() { $this->expectException(UserNotFoundException::class); - $provider = new InMemoryUserProvider(); - $provider->loadUserByIdentifier('fabien'); + (new InMemoryUserProvider())->loadUserByIdentifier('fabien'); } } diff --git a/src/Symfony/Component/Security/Core/Tests/Validator/Constraints/UserPasswordValidatorTestCase.php b/src/Symfony/Component/Security/Core/Tests/Validator/Constraints/UserPasswordValidatorTestCase.php index ccf556a01e240..c78f6b5f3d02a 100644 --- a/src/Symfony/Component/Security/Core/Tests/Validator/Constraints/UserPasswordValidatorTestCase.php +++ b/src/Symfony/Component/Security/Core/Tests/Validator/Constraints/UserPasswordValidatorTestCase.php @@ -113,13 +113,14 @@ public static function emptyPasswordData() public function testUserIsNotValid() { - $this->expectException(ConstraintDefinitionException::class); $user = new \stdClass(); $this->tokenStorage = $this->createTokenStorage($user); $this->validator = $this->createValidator(); $this->validator->initialize($this->context); + $this->expectException(ConstraintDefinitionException::class); + $this->validator->validate('secret', new UserPassword()); } diff --git a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php index 64e618031f7de..593d1a781f81d 100644 --- a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php +++ b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php @@ -94,8 +94,10 @@ public function testGetNonExistingTokenFromClosedSession() public function testGetNonExistingTokenFromActiveSession() { - $this->expectException(TokenNotFoundException::class); $this->session->start(); + + $this->expectException(TokenNotFoundException::class); + $this->storage->getToken('token_id'); } diff --git a/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcTokenHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcTokenHandlerTest.php index ccf11e49862b6..ae3ca5308b06a 100644 --- a/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcTokenHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcTokenHandlerTest.php @@ -80,12 +80,12 @@ public static function getClaims(): iterable */ public function testThrowsAnErrorIfTokenIsInvalid(string $token) { - $this->expectException(BadCredentialsException::class); - $this->expectExceptionMessage('Invalid credentials.'); - $loggerMock = $this->createMock(LoggerInterface::class); $loggerMock->expects($this->once())->method('error'); + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('Invalid credentials.'); + (new OidcTokenHandler( new ES256(), $this->getJWK(), @@ -128,9 +128,6 @@ public static function getInvalidTokens(): iterable public function testThrowsAnErrorIfUserPropertyIsMissing() { - $this->expectException(BadCredentialsException::class); - $this->expectExceptionMessage('Invalid credentials.'); - $loggerMock = $this->createMock(LoggerInterface::class); $loggerMock->expects($this->once())->method('error'); @@ -145,6 +142,9 @@ public function testThrowsAnErrorIfUserPropertyIsMissing() ]; $token = $this->buildJWS(json_encode($claims)); + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('Invalid credentials.'); + (new OidcTokenHandler( new ES256(), self::getJWK(), diff --git a/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcUserInfoTokenHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcUserInfoTokenHandlerTest.php index 2c8d9ae803f9d..40eb5ce81d616 100644 --- a/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcUserInfoTokenHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcUserInfoTokenHandlerTest.php @@ -63,15 +63,10 @@ public static function getClaims(): iterable public function testThrowsAnExceptionIfUserPropertyIsMissing() { - $this->expectException(BadCredentialsException::class); - $this->expectExceptionMessage('Invalid credentials.'); - - $response = ['foo' => 'bar']; - $responseMock = $this->createMock(ResponseInterface::class); $responseMock->expects($this->once()) ->method('toArray') - ->willReturn($response); + ->willReturn(['foo' => 'bar']); $clientMock = $this->createMock(HttpClientInterface::class); $clientMock->expects($this->once()) @@ -83,6 +78,10 @@ public function testThrowsAnExceptionIfUserPropertyIsMissing() ->method('error'); $handler = new OidcUserInfoTokenHandler($clientMock, $loggerMock); + + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('Invalid credentials.'); + $handler->getUserBadgeFrom('a-secret-token'); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/ChainedAccessTokenExtractorsTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/ChainedAccessTokenExtractorsTest.php index 1507e425726a6..f7ef04c6e701b 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/ChainedAccessTokenExtractorsTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/ChainedAccessTokenExtractorsTest.php @@ -67,13 +67,13 @@ public function testAuthenticate() /** * @dataProvider provideInvalidAuthenticateData */ - public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) + public function testAuthenticateInvalid(Request $request, string $errorMessage, string $exceptionType) { + $this->setUpAuthenticator(); + $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); - $this->setUpAuthenticator(); - $this->authenticator->authenticate($request); } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/FormEncodedBodyAccessTokenAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/FormEncodedBodyAccessTokenAuthenticatorTest.php index 3299f01729104..11443a1c92992 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/FormEncodedBodyAccessTokenAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/FormEncodedBodyAccessTokenAuthenticatorTest.php @@ -82,13 +82,13 @@ public function testAuthenticateWithCustomParameter() /** * @dataProvider provideInvalidAuthenticateData */ - public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) + public function testAuthenticateInvalid(Request $request, string $errorMessage, string $exceptionType) { + $this->setUpAuthenticator(); + $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); - $this->setUpAuthenticator(); - $this->authenticator->authenticate($request); } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/HeaderAccessTokenAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/HeaderAccessTokenAuthenticatorTest.php index de85e66fdf372..23910af80e3d9 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/HeaderAccessTokenAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/HeaderAccessTokenAuthenticatorTest.php @@ -110,13 +110,13 @@ public function testAuthenticateWithCustomTokenType() /** * @dataProvider provideInvalidAuthenticateData */ - public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) + public function testAuthenticateInvalid(Request $request, string $errorMessage, string $exceptionType) { + $this->setUpAuthenticator(); + $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); - $this->setUpAuthenticator(); - $this->authenticator->authenticate($request); } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/QueryAccessTokenAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/QueryAccessTokenAuthenticatorTest.php index 428b1fd08ea2b..00fa650841e2d 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/QueryAccessTokenAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessToken/QueryAccessTokenAuthenticatorTest.php @@ -78,13 +78,13 @@ public function testAuthenticateWithCustomParameter() /** * @dataProvider provideInvalidAuthenticateData */ - public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) + public function testAuthenticateInvalid(Request $request, string $errorMessage, string $exceptionType) { + $this->setUpAuthenticator(); + $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); - $this->setUpAuthenticator(); - $this->authenticator->authenticate($request); } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessTokenAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessTokenAuthenticatorTest.php index 4f010000429dd..3279b8520d4d2 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessTokenAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AccessTokenAuthenticatorTest.php @@ -37,9 +37,6 @@ protected function setUp(): void public function testAuthenticateWithoutAccessToken() { - $this->expectException(BadCredentialsException::class); - $this->expectExceptionMessage('Invalid credentials.'); - $request = Request::create('/test'); $this->accessTokenExtractor @@ -53,6 +50,9 @@ public function testAuthenticateWithoutAccessToken() $this->accessTokenExtractor, ); + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('Invalid credentials.'); + $authenticator->authenticate($request); } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php index b0b44d94ea73b..af5c4fad267be 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php @@ -72,13 +72,14 @@ public static function provideUsernamesForLength() */ public function testHandleNonStringUsernameWithArray($postOnly) { - $this->expectException(BadRequestHttpException::class); - $this->expectExceptionMessage('The key "_username" must be a string, "array" given.'); - $request = Request::create('/login_check', 'POST', ['_username' => []]); $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); + + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "array" given.'); + $this->authenticator->authenticate($request); } @@ -87,13 +88,14 @@ public function testHandleNonStringUsernameWithArray($postOnly) */ public function testHandleNonStringUsernameWithInt($postOnly) { - $this->expectException(BadRequestHttpException::class); - $this->expectExceptionMessage('The key "_username" must be a string, "integer" given.'); - $request = Request::create('/login_check', 'POST', ['_username' => 42]); $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); + + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "integer" given.'); + $this->authenticator->authenticate($request); } @@ -102,13 +104,14 @@ public function testHandleNonStringUsernameWithInt($postOnly) */ public function testHandleNonStringUsernameWithObject($postOnly) { - $this->expectException(BadRequestHttpException::class); - $this->expectExceptionMessage('The key "_username" must be a string, "object" given.'); - $request = Request::create('/login_check', 'POST', ['_username' => new \stdClass()]); $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); + + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "object" given.'); + $this->authenticator->authenticate($request); } @@ -132,13 +135,14 @@ public function testHandleNonStringUsernameWithToString($postOnly) */ public function testHandleNonStringPasswordWithArray(bool $postOnly) { - $this->expectException(BadRequestHttpException::class); - $this->expectExceptionMessage('The key "_password" must be a string, "array" given.'); - $request = Request::create('/login_check', 'POST', ['_username' => 'foo', '_password' => []]); $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); + + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_password" must be a string, "array" given.'); + $this->authenticator->authenticate($request); } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php index 21b2203c830e9..2bac2e0a789fd 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php @@ -93,13 +93,13 @@ public function testAuthenticateWithCustomPath() /** * @dataProvider provideInvalidAuthenticateData */ - public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) + public function testAuthenticateInvalid(Request $request, string $errorMessage, string $exceptionType = BadRequestHttpException::class) { + $this->setUpAuthenticator(); + $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); - $this->setUpAuthenticator(); - $this->authenticator->authenticate($request); } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/LoginLinkAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/LoginLinkAuthenticatorTest.php index 5d8088f4fb208..08af3a378894b 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/LoginLinkAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/LoginLinkAuthenticatorTest.php @@ -79,7 +79,6 @@ public function testSuccessfulAuthenticate() public function testUnsuccessfulAuthenticate() { - $this->expectException(InvalidLoginLinkAuthenticationException::class); $this->setUpAuthenticator(); $request = Request::create('/login/link/check?stuff=1&user=weaverryan'); @@ -89,13 +88,15 @@ public function testUnsuccessfulAuthenticate() ->willThrowException(new ExpiredLoginLinkException()); $passport = $this->authenticator->authenticate($request); + + $this->expectException(InvalidLoginLinkAuthenticationException::class); + // trigger the user loader to try to load the user $passport->getBadge(UserBadge::class)->getUser(); } public function testMissingUser() { - $this->expectException(InvalidLoginLinkAuthenticationException::class); $this->setUpAuthenticator(); $request = Request::create('/login/link/check?stuff=1'); @@ -103,6 +104,8 @@ public function testMissingUser() $this->loginLinkHandler->expects($this->never()) ->method('consumeLoginLink'); + $this->expectException(InvalidLoginLinkAuthenticationException::class); + $this->authenticator->authenticate($request); } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php index 52bb1a61d9ca1..2c8e70585072d 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php @@ -86,17 +86,19 @@ public function testAuthenticateWithoutToken() public function testAuthenticateWithoutOldToken() { + $request = Request::create('/', 'GET', [], ['_remember_me_cookie' => base64_encode('foo:bar')]); + $this->expectException(AuthenticationException::class); - $request = Request::create('/', 'GET', [], ['_remember_me_cookie' => base64_encode('foo:bar')]); $this->authenticator->authenticate($request); } public function testAuthenticateWithTokenWithoutDelimiter() { + $request = Request::create('/', 'GET', [], ['_remember_me_cookie' => 'invalid']); + $this->expectException(AuthenticationException::class); - $request = Request::create('/', 'GET', [], ['_remember_me_cookie' => 'invalid']); $this->authenticator->authenticate($request); } } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php index 85a9b8b78e465..1ade1bf0a7c57 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php @@ -43,7 +43,7 @@ protected function setUp(): void /** * @dataProvider providePasswords */ - public function testPasswordAuthenticated($password, $passwordValid, $result) + public function testPasswordAuthenticated(string $password, bool $passwordValid, bool $result) { $hasher = $this->createMock(PasswordHasherInterface::class); $hasher->expects($this->any())->method('verify')->with('password-hash', $password)->willReturn($passwordValid); @@ -71,19 +71,22 @@ public static function providePasswords() public function testEmptyPassword() { + $this->hasherFactory + ->expects($this->never()) + ->method('getPasswordHasher'); + + $event = $this->createEvent(new Passport(new UserBadge('wouter', fn () => $this->user), new PasswordCredentials(''))); + $this->expectException(BadCredentialsException::class); $this->expectExceptionMessage('The presented password cannot be empty.'); - $this->hasherFactory->expects($this->never())->method('getPasswordHasher'); - - $event = $this->createEvent(new Passport(new UserBadge('wouter', fn () => $this->user), new PasswordCredentials(''))); $this->listener->checkPassport($event); } /** * @dataProvider provideCustomAuthenticatedResults */ - public function testCustomAuthenticated($result) + public function testCustomAuthenticated(bool $result) { $this->hasherFactory->expects($this->never())->method('getPasswordHasher'); diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php index 7942616b2a396..b591c5ef3c0b5 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php @@ -58,15 +58,16 @@ public function testValidCsrfToken() public function testInvalidCsrfToken() { - $this->expectException(InvalidCsrfTokenException::class); - $this->expectExceptionMessage('Invalid CSRF token.'); - $this->csrfTokenManager->expects($this->any()) ->method('isTokenValid') ->with(new CsrfToken('authenticator_token_id', 'abc123')) ->willReturn(false); $event = $this->createEvent($this->createPassport(new CsrfTokenBadge('authenticator_token_id', 'abc123'))); + + $this->expectException(InvalidCsrfTokenException::class); + $this->expectExceptionMessage('Invalid CSRF token.'); + $this->listener->checkPassport($event); } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php index 3f5f2ff7a01c7..2d03b7ac357ea 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/IsGrantedAttributeListenerTest.php @@ -191,8 +191,6 @@ public function testIsGrantedArrayWithNullValueSubjectFromArguments() public function testExceptionWhenMissingSubjectAttribute() { - $this->expectException(\RuntimeException::class); - $authChecker = $this->createMock(AuthorizationCheckerInterface::class); $event = new ControllerArgumentsEvent( @@ -204,6 +202,9 @@ public function testExceptionWhenMissingSubjectAttribute() ); $listener = new IsGrantedAttributeListener($authChecker); + + $this->expectException(\RuntimeException::class); + $listener->onKernelControllerArguments($event); } @@ -261,9 +262,6 @@ public static function getAccessDeniedMessageTests() public function testNotFoundHttpException() { - $this->expectException(HttpException::class); - $this->expectExceptionMessage('Not found'); - $authChecker = $this->createMock(AuthorizationCheckerInterface::class); $authChecker->expects($this->any()) ->method('isGranted') @@ -278,6 +276,10 @@ public function testNotFoundHttpException() ); $listener = new IsGrantedAttributeListener($authChecker); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Not found'); + $listener->onKernelControllerArguments($event); } @@ -387,10 +389,6 @@ public function testIsGrantedWithRequestAsSubject() public function testHttpExceptionWithExceptionCode() { - $this->expectException(HttpException::class); - $this->expectExceptionMessage('Exception Code'); - $this->expectExceptionCode(10010); - $authChecker = $this->createMock(AuthorizationCheckerInterface::class); $authChecker->expects($this->any()) ->method('isGranted') @@ -405,15 +403,16 @@ public function testHttpExceptionWithExceptionCode() ); $listener = new IsGrantedAttributeListener($authChecker); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Exception Code'); + $this->expectExceptionCode(10010); + $listener->onKernelControllerArguments($event); } public function testAccessDeniedExceptionWithExceptionCode() { - $this->expectException(AccessDeniedException::class); - $this->expectExceptionMessage('Exception Code'); - $this->expectExceptionCode(10010); - $authChecker = $this->createMock(AuthorizationCheckerInterface::class); $authChecker->expects($this->any()) ->method('isGranted') @@ -428,6 +427,11 @@ public function testAccessDeniedExceptionWithExceptionCode() ); $listener = new IsGrantedAttributeListener($authChecker); + + $this->expectException(AccessDeniedException::class); + $this->expectExceptionMessage('Exception Code'); + $this->expectExceptionCode(10010); + $listener->onKernelControllerArguments($event); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php index 181454e43ec33..e9bc31587ffba 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php @@ -32,7 +32,6 @@ class AccessListenerTest extends TestCase { public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess() { - $this->expectException(AccessDeniedException::class); $request = new Request(); $accessMap = $this->createMock(AccessMapInterface::class); @@ -70,6 +69,8 @@ public function getCredentials(): mixed $accessMap ); + $this->expectException(AccessDeniedException::class); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } @@ -131,7 +132,6 @@ public function testHandleWhenAccessMapReturnsEmptyAttributes() public function testHandleWhenTheSecurityTokenStorageHasNoToken() { - $this->expectException(AccessDeniedException::class); $tokenStorage = new TokenStorage(); $request = new Request(); @@ -155,6 +155,8 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken() false ); + $this->expectException(AccessDeniedException::class); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php index 06139bcca1aff..c7cdc7abd216a 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php @@ -122,8 +122,6 @@ public function testHandleMatchedPathWithoutCsrfValidation() public function testNoResponseSet() { - $this->expectException(\RuntimeException::class); - [$listener, , $httpUtils, $options] = $this->getListener(); $request = new Request(); @@ -133,6 +131,8 @@ public function testNoResponseSet() ->with($request, $options['logout_path']) ->willReturn(true); + $this->expectException(\RuntimeException::class); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } @@ -141,7 +141,6 @@ public function testNoResponseSet() */ public function testCsrfValidationFails($invalidToken) { - $this->expectException(LogoutException::class); $tokenManager = $this->getTokenManager(); [$listener, , $httpUtils, $options] = $this->getListener(null, $tokenManager); @@ -160,6 +159,8 @@ public function testCsrfValidationFails($invalidToken) ->method('isTokenValid') ->willReturn(false); + $this->expectException(LogoutException::class); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php index 916e54d669376..46da56485d529 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php @@ -68,22 +68,26 @@ public function testEventIsIgnoredIfUsernameIsNotPassedWithTheRequest() public function testExitUserThrowsAuthenticationExceptionIfNoCurrentToken() { - $this->expectException(AuthenticationCredentialsNotFoundException::class); $this->tokenStorage->setToken(null); $this->request->query->set('_switch_user', '_exit'); $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); + + $this->expectException(AuthenticationCredentialsNotFoundException::class); + $listener($this->event); } public function testExitUserThrowsAuthenticationExceptionIfOriginalTokenCannotBeFound() { - $this->expectException(AuthenticationCredentialsNotFoundException::class); $token = new UsernamePasswordToken(new InMemoryUser('username', '', ['ROLE_FOO']), 'key', ['ROLE_FOO']); $this->tokenStorage->setToken($token); $this->request->query->set('_switch_user', SwitchUserListener::EXIT_VALUE); $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); + + $this->expectException(AuthenticationCredentialsNotFoundException::class); + $listener($this->event); } @@ -134,7 +138,6 @@ public function testExitUserDispatchesEventWithRefreshedUser() public function testSwitchUserIsDisallowed() { - $this->expectException(AccessDeniedException::class); $token = new UsernamePasswordToken(new InMemoryUser('username', '', ['ROLE_FOO']), 'key', ['ROLE_FOO']); $user = new InMemoryUser('username', 'password', []); @@ -146,12 +149,14 @@ public function testSwitchUserIsDisallowed() ->willReturn(false); $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); + + $this->expectException(AccessDeniedException::class); + $listener($this->event); } public function testSwitchUserTurnsAuthenticationExceptionTo403() { - $this->expectException(AccessDeniedException::class); $token = new UsernamePasswordToken(new InMemoryUser('username', '', ['ROLE_ALLOWED_TO_SWITCH']), 'key', ['ROLE_ALLOWED_TO_SWITCH']); $this->tokenStorage->setToken($token); @@ -161,6 +166,9 @@ public function testSwitchUserTurnsAuthenticationExceptionTo403() ->method('decide'); $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); + + $this->expectException(AccessDeniedException::class); + $listener($this->event); } @@ -303,10 +311,12 @@ public function testSwitchUserWithReplacedToken() public function testSwitchUserThrowsAuthenticationExceptionIfNoCurrentToken() { - $this->expectException(AuthenticationCredentialsNotFoundException::class); $this->tokenStorage->setToken(null); $this->request->query->set('_switch_user', 'username'); $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); + + $this->expectException(AuthenticationCredentialsNotFoundException::class); + $listener($this->event); } diff --git a/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php b/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php index e165a4df52c4d..ccb538f953df9 100644 --- a/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php +++ b/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php @@ -306,7 +306,6 @@ public function testCheckRequestPathWithUrlMatcherAndResourceFoundByRequest() public function testCheckRequestPathWithUrlMatcherLoadingException() { - $this->expectException(\RuntimeException::class); $urlMatcher = $this->createMock(UrlMatcherInterface::class); $urlMatcher ->expects($this->any()) @@ -315,6 +314,9 @@ public function testCheckRequestPathWithUrlMatcherLoadingException() ; $utils = new HttpUtils(null, $urlMatcher); + + $this->expectException(\RuntimeException::class); + $utils->checkRequestPath($this->getRequest(), 'foobar'); } @@ -369,8 +371,7 @@ public function testUrlGeneratorIsRequiredToGenerateUrl() { $this->expectException(\LogicException::class); $this->expectExceptionMessage('You must provide a UrlGeneratorInterface instance to be able to use routes.'); - $utils = new HttpUtils(); - $utils->generateUri(new Request(), 'route_name'); + (new HttpUtils())->generateUri(new Request(), 'route_name'); } private function getUrlGenerator($generatedUrl = '/foo/bar') diff --git a/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php b/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php index c0e5dcbe38521..7073f35496006 100644 --- a/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php @@ -46,9 +46,10 @@ public function testGetLogoutPath() public function testGetLogoutPathWithoutLogoutListenerRegisteredForKeyThrowsException() { + $this->generator->registerListener('secured_area', '/logout', null, null, null); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('No LogoutListener found for firewall key "unregistered_key".'); - $this->generator->registerListener('secured_area', '/logout', null, null, null); $this->generator->getLogoutPath('unregistered_key'); } @@ -88,20 +89,22 @@ public function testGuessFromTokenWithoutFirewallNameFallbacksToCurrentFirewall( public function testUnableToGuessWithoutCurrentFirewallThrowsException() { + $this->generator->registerListener('secured_area', '/logout', null, null); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('This request is not behind a firewall, pass the firewall name manually to generate a logout URL.'); - $this->generator->registerListener('secured_area', '/logout', null, null); $this->generator->getLogoutPath(); } public function testUnableToGuessWithCurrentFirewallThrowsException() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unable to find logout in the current firewall, pass the firewall name manually to generate a logout URL.'); $this->generator->registerListener('secured_area', '/logout', null, null); $this->generator->setCurrentFirewall('admin'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to find logout in the current firewall, pass the firewall name manually to generate a logout URL.'); + $this->generator->getLogoutPath(); } } diff --git a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php index 39c7a9f3ed7b8..80753fcebb0c2 100644 --- a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php @@ -129,8 +129,6 @@ public function testConsumeRememberMeCookieValidByValidatorWithoutUpdate() public function testConsumeRememberMeCookieInvalidToken() { - $this->expectException(CookieTheftException::class); - $this->tokenProvider->expects($this->any()) ->method('loadTokenBySeries') ->with('series1') @@ -138,14 +136,13 @@ public function testConsumeRememberMeCookieInvalidToken() $this->tokenProvider->expects($this->never())->method('updateToken')->with('series1'); + $this->expectException(CookieTheftException::class); + $this->handler->consumeRememberMeCookie(new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'series1:tokenvalue')); } public function testConsumeRememberMeCookieExpired() { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('The cookie has expired.'); - $this->tokenProvider->expects($this->any()) ->method('loadTokenBySeries') ->with('series1') @@ -153,6 +150,9 @@ public function testConsumeRememberMeCookieExpired() $this->tokenProvider->expects($this->never())->method('updateToken')->with('series1'); + $this->expectException(AuthenticationException::class); + $this->expectExceptionMessage('The cookie has expired.'); + $this->handler->consumeRememberMeCookie(new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'series1:tokenvalue')); } diff --git a/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php index b52b2f5a522c8..158baf68a330a 100644 --- a/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php @@ -31,12 +31,14 @@ public function testSessionIsNotChanged() public function testUnsupportedStrategy() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Invalid session authentication strategy "foo"'); $request = $this->getRequest(); $request->expects($this->never())->method('getSession'); $strategy = new SessionAuthenticationStrategy('foo'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Invalid session authentication strategy "foo"'); + $strategy->onAuthentication($request, $this->createMock(TokenInterface::class)); } diff --git a/src/Symfony/Component/Serializer/Tests/Annotation/SerializedNameTest.php b/src/Symfony/Component/Serializer/Tests/Annotation/SerializedNameTest.php index c2b5e5f2ab6b3..3a829aecf4f84 100644 --- a/src/Symfony/Component/Serializer/Tests/Annotation/SerializedNameTest.php +++ b/src/Symfony/Component/Serializer/Tests/Annotation/SerializedNameTest.php @@ -30,7 +30,7 @@ public function testNotAStringSerializedNameParameter() public function testSerializedNameParameters() { - $maxDepth = new SerializedName('foo'); - $this->assertEquals('foo', $maxDepth->getSerializedName()); + $foo = new SerializedName('foo'); + $this->assertEquals('foo', $foo->getSerializedName()); } } diff --git a/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php b/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php index eb77263f49fc9..037eafdb66665 100644 --- a/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php +++ b/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php @@ -28,20 +28,20 @@ class SerializerPassTest extends TestCase { public function testThrowExceptionWhenNoNormalizers() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('You must tag at least one service as "serializer.normalizer" to use the "serializer" service'); $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); $container->register('serializer'); $serializerPass = new SerializerPass(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('You must tag at least one service as "serializer.normalizer" to use the "serializer" service'); + $serializerPass->process($container); } public function testThrowExceptionWhenNoEncoders() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('You must tag at least one service as "serializer.encoder" to use the "serializer" service'); $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); $container->register('serializer') @@ -50,6 +50,10 @@ public function testThrowExceptionWhenNoEncoders() $container->register('normalizer')->addTag('serializer.normalizer'); $serializerPass = new SerializerPass(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('You must tag at least one service as "serializer.encoder" to use the "serializer" service'); + $serializerPass->process($container); } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/JsonDecodeTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/JsonDecodeTest.php index 66cd10114cc90..f336bcd42f8a9 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/JsonDecodeTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/JsonDecodeTest.php @@ -47,11 +47,9 @@ public static function decodeProvider() $stdClass = new \stdClass(); $stdClass->foo = 'bar'; - $assoc = ['foo' => 'bar']; - return [ ['{"foo": "bar"}', $stdClass, []], - ['{"foo": "bar"}', $assoc, ['json_decode_associative' => true]], + ['{"foo": "bar"}', ['foo' => 'bar'], ['json_decode_associative' => true]], ]; } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php index 1b47684ae1c8d..a34e82c6b09a5 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php @@ -84,12 +84,13 @@ public function testWithDefaultContext() public function testEncodeNotUtf8WithoutPartialOnError() { - $this->expectException(UnexpectedValueException::class); $arr = [ 'utf8' => 'Hello World!', 'notUtf8' => "\xb0\xd0\xb5\xd0", ]; + $this->expectException(UnexpectedValueException::class); + $this->encoder->encode($arr, 'json'); } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CacheMetadataFactoryTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CacheMetadataFactoryTest.php index 9525ca6059fb3..6db0b95ae2403 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CacheMetadataFactoryTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CacheMetadataFactoryTest.php @@ -58,10 +58,11 @@ public function testHasMetadataFor() public function testInvalidClassThrowsException() { - $this->expectException(InvalidArgumentException::class); $decorated = $this->createMock(ClassMetadataFactoryInterface::class); $factory = new CacheClassMetadataFactory($decorated, new ArrayAdapter()); + $this->expectException(InvalidArgumentException::class); + $factory->getMetadataFor('Not\Exist'); } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php index 683f445dfe2b0..ff54fb96b7af1 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php @@ -34,19 +34,21 @@ public function testItImplementsClassMetadataFactoryInterface() public function testItThrowAnExceptionWhenCacheFileIsNotFound() { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $this->expectException(\RuntimeException::class); $this->expectExceptionMessageMatches('#File ".*/Fixtures/not-found-serializer.class.metadata.php" could not be found.#'); - $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/not-found-serializer.class.metadata.php', $classMetadataFactory); } public function testItThrowAnExceptionWhenMetadataIsNotOfTypeArray() { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Compiled metadata must be of the type array, object given.'); - $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/object-metadata.php', $classMetadataFactory); } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php index ea81a9d8ad7cd..48e95aecd9245 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -65,8 +65,10 @@ public function testLoadClassMetadataReturnsFalseWhenEmpty() public function testLoadClassMetadataReturnsThrowsInvalidMapping() { - $this->expectException(MappingException::class); $loader = new YamlFileLoader(__DIR__.'/../../Fixtures/invalid-mapping.yml'); + + $this->expectException(MappingException::class); + $loader->loadClassMetadata($this->metadata); } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 0b91fc0dbc288..ca3c7579be301 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -82,10 +82,12 @@ public function testInstantiateObjectDenormalizer() public function testDenormalizeWithExtraAttribute() { - $this->expectException(ExtraAttributesException::class); - $this->expectExceptionMessage('Extra attributes are not allowed ("fooFoo" is unknown).'); $factory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new AbstractObjectNormalizerDummy($factory); + + $this->expectException(ExtraAttributesException::class); + $this->expectExceptionMessage('Extra attributes are not allowed ("fooFoo" is unknown).'); + $normalizer->denormalize( ['fooFoo' => 'foo'], Dummy::class, @@ -96,10 +98,12 @@ public function testDenormalizeWithExtraAttribute() public function testDenormalizeWithExtraAttributes() { - $this->expectException(ExtraAttributesException::class); - $this->expectExceptionMessage('Extra attributes are not allowed ("fooFoo", "fooBar" are unknown).'); $factory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new AbstractObjectNormalizerDummy($factory); + + $this->expectException(ExtraAttributesException::class); + $this->expectExceptionMessage('Extra attributes are not allowed ("fooFoo", "fooBar" are unknown).'); + $normalizer->denormalize( ['fooFoo' => 'foo', 'fooBar' => 'bar'], Dummy::class, @@ -110,9 +114,11 @@ public function testDenormalizeWithExtraAttributes() public function testDenormalizeWithExtraAttributesAndNoGroupsWithMetadataFactory() { + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $this->expectException(ExtraAttributesException::class); $this->expectExceptionMessage('Extra attributes are not allowed ("fooFoo", "fooBar" are unknown).'); - $normalizer = new AbstractObjectNormalizerWithMetadata(); + $normalizer->denormalize( ['fooFoo' => 'foo', 'fooBar' => 'bar', 'bar' => 'bar'], Dummy::class, @@ -134,9 +140,11 @@ public function testDenormalizePlainObject() public function testDenormalizeWithDuplicateNestedAttributes() { + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $this->expectException(LogicException::class); $this->expectExceptionMessage('Duplicate serialized path: "one,two,three" used for properties "foo" and "bar".'); - $normalizer = new AbstractObjectNormalizerWithMetadata(); + $normalizer->denormalize([], DuplicateValueNestedDummy::class, 'any'); } @@ -204,8 +212,6 @@ public function testDenormalizeWithNestedAttributes() public function testDenormalizeWithNestedAttributesDuplicateKeys() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Duplicate values for key "quux" found. One value is set via the SerializedPath attribute: "one->four", the other one is set via the SerializedName attribute: "notquux".'); $normalizer = new AbstractObjectNormalizerWithMetadata(); $data = [ 'one' => [ @@ -213,6 +219,10 @@ public function testDenormalizeWithNestedAttributesDuplicateKeys() ], 'quux' => 'notquux', ]; + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Duplicate values for key "quux" found. One value is set via the SerializedPath attribute: "one->four", the other one is set via the SerializedName attribute: "notquux".'); + $normalizer->denormalize($data, DuplicateKeyNestedDummy::class, 'any'); } @@ -265,25 +275,29 @@ public function testDenormalizeWithNestedAttributesInConstructorAndDiscriminator public function testNormalizeWithNestedAttributesMixingArrayTypes() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('The element you are trying to set is already populated: "[one][two]"'); $foobar = new AlreadyPopulatedNestedDummy(); $foobar->foo = 'foo'; $foobar->bar = 'bar'; $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The element you are trying to set is already populated: "[one][two]"'); + $normalizer->normalize($foobar, 'any'); } public function testNormalizeWithNestedAttributesElementAlreadySet() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('The element you are trying to set is already populated: "[one][two][three]"'); $foobar = new DuplicateValueNestedDummy(); $foobar->foo = 'foo'; $foobar->bar = 'bar'; $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The element you are trying to set is already populated: "[one][two][three]"'); + $normalizer->normalize($foobar, 'any'); } @@ -691,9 +705,10 @@ private function getDenormalizerForObjectWithBasicProperties() */ public function testExtraAttributesException() { + $normalizer = new ObjectNormalizer(); + $this->expectException(LogicException::class); $this->expectExceptionMessage('A class metadata factory must be provided in the constructor when setting "allow_extra_attributes" to false.'); - $normalizer = new ObjectNormalizer(); $normalizer->denormalize([], \stdClass::class, 'xml', [ 'allow_extra_attributes' => false, diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DataUriNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DataUriNormalizerTest.php index 92e173fe096ad..7e9af436038fe 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DataUriNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DataUriNormalizerTest.php @@ -121,7 +121,7 @@ public function testGiveNotAccessToLocalFiles() /** * @dataProvider invalidUriProvider */ - public function testInvalidData($uri) + public function testInvalidData(?string $uri) { $this->expectException(UnexpectedValueException::class); $this->normalizer->denormalize($uri, 'SplFileObject'); @@ -148,7 +148,7 @@ public static function invalidUriProvider() /** * @dataProvider validUriProvider */ - public function testValidData($uri) + public function testValidData(string $uri) { $this->assertInstanceOf(\SplFileObject::class, $this->normalizer->denormalize($uri, 'SplFileObject')); } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 1d471981e4f0e..eb2b6530678c2 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -374,8 +374,6 @@ protected function getDenormalizerForIgnoredAttributes(): GetSetMethodNormalizer public function testUnableToNormalizeObjectAttribute() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Cannot normalize attribute "object" because the injected serializer is not a normalizer'); $serializer = $this->createMock(SerializerInterface::class); $this->normalizer->setSerializer($serializer); @@ -383,6 +381,9 @@ public function testUnableToNormalizeObjectAttribute() $object = new \stdClass(); $obj->setObject($object); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cannot normalize attribute "object" because the injected serializer is not a normalizer'); + $this->normalizer->normalize($obj, 'any'); } @@ -391,14 +392,12 @@ public function testSiblingReference() $serializer = new Serializer([$this->normalizer]); $this->normalizer->setSerializer($serializer); - $siblingHolder = new SiblingHolder(); - $expected = [ 'sibling0' => ['coopTilleuls' => 'Les-Tilleuls.coop'], 'sibling1' => ['coopTilleuls' => 'Les-Tilleuls.coop'], 'sibling2' => ['coopTilleuls' => 'Les-Tilleuls.coop'], ]; - $this->assertEquals($expected, $this->normalizer->normalize($siblingHolder)); + $this->assertEquals($expected, $this->normalizer->normalize(new SiblingHolder())); } public function testDenormalizeNonExistingAttribute() diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php index 54a977f55ec3b..f8f8546d7cb39 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php @@ -68,9 +68,10 @@ public function testNormalize() public function testCircularNormalize() { - $this->expectException(CircularReferenceException::class); $this->createNormalizer([JsonSerializableNormalizer::CIRCULAR_REFERENCE_LIMIT => 1]); + $this->expectException(CircularReferenceException::class); + $this->serializer ->expects($this->once()) ->method('normalize') diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index f9f2e8ad040d6..05b1891f50119 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -307,8 +307,6 @@ public function testConstructorWithUnconstructableNullableObjectTypeHintDenormal public function testConstructorWithUnknownObjectTypeHintDenormalize() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Could not determine the class of the parameter "unknown".'); $data = [ 'id' => 10, 'unknown' => [ @@ -321,6 +319,9 @@ public function testConstructorWithUnknownObjectTypeHintDenormalize() $serializer = new Serializer([$normalizer]); $normalizer->setSerializer($serializer); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Could not determine the class of the parameter "unknown".'); + $normalizer->denormalize($data, DummyWithConstructorInexistingObject::class); } @@ -623,8 +624,6 @@ protected function getDenormalizerForTypeEnforcement(): ObjectNormalizer public function testUnableToNormalizeObjectAttribute() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Cannot normalize attribute "object" because the injected serializer is not a normalizer'); $serializer = $this->createMock(SerializerInterface::class); $this->normalizer->setSerializer($serializer); @@ -632,6 +631,9 @@ public function testUnableToNormalizeObjectAttribute() $object = new \stdClass(); $obj->setObject($object); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cannot normalize attribute "object" because the injected serializer is not a normalizer'); + $this->normalizer->normalize($obj, 'any'); } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 631111d2a2b6c..585c2068ec682 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -400,8 +400,6 @@ public function testDenormalizeShouldIgnoreStaticProperty() public function testUnableToNormalizeObjectAttribute() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Cannot normalize attribute "bar" because the injected serializer is not a normalizer'); $serializer = $this->createMock(SerializerInterface::class); $this->normalizer->setSerializer($serializer); @@ -409,6 +407,9 @@ public function testUnableToNormalizeObjectAttribute() $object = new \stdClass(); $obj->setBar($object); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cannot normalize attribute "bar" because the injected serializer is not a normalizer'); + $this->normalizer->normalize($obj, 'any'); } diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 6248531076558..45d467064e306 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -97,8 +97,10 @@ public function testItThrowsExceptionOnInvalidEncoder() public function testNormalizeNoMatch() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([$this->createMock(NormalizerInterface::class)]); + + $this->expectException(UnexpectedValueException::class); + $serializer->normalize(new \stdClass(), 'xml'); } @@ -118,15 +120,19 @@ public function testNormalizeGivesPriorityToInterfaceOverTraversable() public function testNormalizeOnDenormalizer() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([new TestDenormalizer()], []); + + $this->expectException(UnexpectedValueException::class); + $this->assertTrue($serializer->normalize(new \stdClass(), 'json')); } public function testDenormalizeNoMatch() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([$this->createMock(NormalizerInterface::class)]); + + $this->expectException(UnexpectedValueException::class); + $serializer->denormalize('foo', 'stdClass'); } @@ -140,9 +146,11 @@ public function testDenormalizeOnObjectThatOnlySupportsDenormalization() public function testDenormalizeOnNormalizer() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([new TestNormalizer()], []); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(UnexpectedValueException::class); + $this->assertTrue($serializer->denormalize(json_encode($data), 'stdClass', 'json')); } @@ -237,17 +245,21 @@ public function testSerializeEmpty() public function testSerializeNoEncoder() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([], []); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(UnexpectedValueException::class); + $serializer->serialize($data, 'json'); } public function testSerializeNoNormalizer() { - $this->expectException(LogicException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(LogicException::class); + $serializer->serialize(Model::fromArray($data), 'json'); } @@ -271,25 +283,31 @@ public function testDeserializeUseCache() public function testDeserializeNoNormalizer() { - $this->expectException(LogicException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(LogicException::class); + $serializer->deserialize(json_encode($data), Model::class, 'json'); } public function testDeserializeWrongNormalizer() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([new CustomNormalizer()], ['json' => new JsonEncoder()]); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(UnexpectedValueException::class); + $serializer->deserialize(json_encode($data), Model::class, 'json'); } public function testDeserializeNoEncoder() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([], []); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(UnexpectedValueException::class); + $serializer->deserialize(json_encode($data), Model::class, 'json'); } @@ -689,29 +707,37 @@ public function testDeserializeScalar() public function testDeserializeLegacyScalarType() { - $this->expectException(LogicException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); + + $this->expectException(LogicException::class); + $serializer->deserialize('42', 'integer', 'json'); } public function testDeserializeScalarTypeToCustomType() { - $this->expectException(LogicException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); + + $this->expectException(LogicException::class); + $serializer->deserialize('"something"', Foo::class, 'json'); } public function testDeserializeNonscalarTypeToScalar() { - $this->expectException(NotNormalizableValueException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); + + $this->expectException(NotNormalizableValueException::class); + $serializer->deserialize('{"foo":true}', 'string', 'json'); } public function testDeserializeInconsistentScalarType() { - $this->expectException(NotNormalizableValueException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); + + $this->expectException(NotNormalizableValueException::class); + $serializer->deserialize('"42"', 'int', 'json'); } @@ -727,8 +753,10 @@ public function testDeserializeScalarArray() public function testDeserializeInconsistentScalarArray() { - $this->expectException(NotNormalizableValueException::class); $serializer = new Serializer([new ArrayDenormalizer()], ['json' => new JsonEncoder()]); + + $this->expectException(NotNormalizableValueException::class); + $serializer->deserialize('["42"]', 'int[]', 'json'); } diff --git a/src/Symfony/Component/Stopwatch/Tests/StopwatchEventTest.php b/src/Symfony/Component/Stopwatch/Tests/StopwatchEventTest.php index 82b3c832a77d0..fcc2436054716 100644 --- a/src/Symfony/Component/Stopwatch/Tests/StopwatchEventTest.php +++ b/src/Symfony/Component/Stopwatch/Tests/StopwatchEventTest.php @@ -122,8 +122,10 @@ public function testDurationWithMultipleStarts() public function testStopWithoutStart() { - $this->expectException(\LogicException::class); $event = new StopwatchEvent(microtime(true) * 1000); + + $this->expectException(\LogicException::class); + $event->stop(); } diff --git a/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php b/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php index 6be89b80efa41..68585d2f8e3c6 100644 --- a/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php +++ b/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php @@ -88,15 +88,15 @@ public function testStop() public function testUnknownEvent() { $this->expectException(\LogicException::class); - $stopwatch = new Stopwatch(); - $stopwatch->getEvent('foo'); + + (new Stopwatch())->getEvent('foo'); } public function testStopWithoutStart() { $this->expectException(\LogicException::class); - $stopwatch = new Stopwatch(); - $stopwatch->stop('foo'); + + (new Stopwatch())->stop('foo'); } public function testMorePrecision() @@ -159,8 +159,8 @@ public function testReopenASection() public function testReopenANewSectionShouldThrowAnException() { $this->expectException(\LogicException::class); - $stopwatch = new Stopwatch(); - $stopwatch->openSection('section'); + + (new Stopwatch())->openSection('section'); } public function testReset() diff --git a/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderFactoryTest.php b/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderFactoryTest.php index 5eac650385b5f..6521656af7d6e 100644 --- a/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderFactoryTest.php +++ b/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderFactoryTest.php @@ -62,13 +62,13 @@ public function testCreate(string $expected, string $dsn) */ public function testUnsupportedSchemeException(string $dsn, string $message) { + $factory = $this->createFactory(); + $dsn = new Dsn($dsn); + $this->expectException(UnsupportedSchemeException::class); $this->expectExceptionMessage($message); - $dsn = new Dsn($dsn); - - $this->createFactory() - ->create($dsn); + $factory->create($dsn); } /** @@ -76,24 +76,24 @@ public function testUnsupportedSchemeException(string $dsn, string $message) */ public function testIncompleteDsnException(string $dsn, string $message) { + $factory = $this->createFactory(); + $dsn = new Dsn($dsn); + $this->expectException(IncompleteDsnException::class); $this->expectExceptionMessage($message); - $dsn = new Dsn($dsn); - - $this->createFactory() - ->create($dsn); + $factory->create($dsn); } public function testRequiredUserAgentOption() { + $factory = $this->createFactory(); + $dsn = new Dsn('phrase://PROJECT_ID:API_TOKEN@default'); + $this->expectException(MissingRequiredOptionException::class); $this->expectExceptionMessage('The option "userAgent" is required but missing.'); - $dsn = new Dsn('phrase://PROJECT_ID:API_TOKEN@default'); - - $this->createFactory() - ->create($dsn); + $factory->create($dsn); } public function testHttpClientConfig() diff --git a/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php b/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php index 089480b1c3d44..fd1c220935b7f 100644 --- a/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php +++ b/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php @@ -379,10 +379,6 @@ public function cacheKeyProvider(): \Generator */ public function testReadProviderExceptions(int $statusCode, string $expectedExceptionMessage, string $expectedLoggerMessage) { - $this->expectException(ProviderExceptionInterface::class); - $this->expectExceptionCode(0); - $this->expectExceptionMessage($expectedExceptionMessage); - $this->getLogger() ->expects(self::once()) ->method('error') @@ -407,6 +403,10 @@ public function testReadProviderExceptions(int $statusCode, string $expectedExce ], ]), endpoint: 'api.phrase.com/api/v2'); + $this->expectException(ProviderExceptionInterface::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage($expectedExceptionMessage); + $provider->read(['messages'], ['en_GB']); } @@ -415,10 +415,6 @@ public function testReadProviderExceptions(int $statusCode, string $expectedExce */ public function testInitLocalesExceptions(int $statusCode, string $expectedExceptionMessage, string $expectedLoggerMessage) { - $this->expectException(ProviderExceptionInterface::class); - $this->expectExceptionCode(0); - $this->expectExceptionMessage($expectedExceptionMessage); - $this->getLogger() ->expects(self::once()) ->method('error') @@ -442,6 +438,10 @@ public function testInitLocalesExceptions(int $statusCode, string $expectedExcep ], ]), endpoint: 'api.phrase.com/api/v2'); + $this->expectException(ProviderExceptionInterface::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage($expectedExceptionMessage); + $provider->read(['messages'], ['en_GB']); } @@ -539,10 +539,6 @@ public function testCreateUnknownLocale() */ public function testCreateLocaleExceptions(int $statusCode, string $expectedExceptionMessage, string $expectedLoggerMessage) { - $this->expectException(ProviderExceptionInterface::class); - $this->expectExceptionCode(0); - $this->expectExceptionMessage($expectedExceptionMessage); - $this->getLogger() ->expects(self::once()) ->method('error') @@ -567,6 +563,10 @@ public function testCreateLocaleExceptions(int $statusCode, string $expectedExce ], ]), endpoint: 'api.phrase.com/api/v2'); + $this->expectException(ProviderExceptionInterface::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage($expectedExceptionMessage); + $provider->read(['messages'], ['nl_NL']); } @@ -627,10 +627,6 @@ public function testDelete() */ public function testDeleteProviderExceptions(int $statusCode, string $expectedExceptionMessage, string $expectedLoggerMessage) { - $this->expectException(ProviderExceptionInterface::class); - $this->expectExceptionCode(0); - $this->expectExceptionMessage($expectedExceptionMessage); - $this->getLogger() ->expects(self::once()) ->method('error') @@ -661,6 +657,10 @@ public function testDeleteProviderExceptions(int $statusCode, string $expectedEx ], ])); + $this->expectException(ProviderExceptionInterface::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage($expectedExceptionMessage); + $provider->delete($bag); } @@ -745,10 +745,6 @@ public function testWrite(string $locale, string $localeId, string $domain, stri */ public function testWriteProviderExceptions(int $statusCode, string $expectedExceptionMessage, string $expectedLoggerMessage) { - $this->expectException(ProviderExceptionInterface::class); - $this->expectExceptionCode(0); - $this->expectExceptionMessage($expectedExceptionMessage); - $this->getLogger() ->expects(self::once()) ->method('error') @@ -784,6 +780,10 @@ public function testWriteProviderExceptions(int $statusCode, string $expectedExc ], ])); + $this->expectException(ProviderExceptionInterface::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage($expectedExceptionMessage); + $provider->write($bag); } diff --git a/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php index ee8e52e06dea0..454783494b574 100644 --- a/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php +++ b/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php @@ -120,11 +120,12 @@ public function testLintSucceedsWhenLocaleInFileAndInTargetLanguageNameUsesDashe public function testLintFileNotReadable() { - $this->expectException(\RuntimeException::class); $tester = $this->createCommandTester(); $filename = $this->createFile(); unlink($filename); + $this->expectException(\RuntimeException::class); + $tester->execute(['filename' => $filename], ['decorated' => false]); } diff --git a/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslationExtractorPassTest.php b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslationExtractorPassTest.php index bcb2ccd454023..574c7338793ed 100644 --- a/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslationExtractorPassTest.php +++ b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslationExtractorPassTest.php @@ -49,14 +49,16 @@ public function testProcessNoDefinitionFound() public function testProcessMissingAlias() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('The alias for the tag "translation.extractor" of service "foo.id" must be set.'); $container = new ContainerBuilder(); $container->register('translation.extractor'); $container->register('foo.id') ->addTag('translation.extractor', []); $translationDumperPass = new TranslationExtractorPass(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The alias for the tag "translation.extractor" of service "foo.id" must be set.'); + $translationDumperPass->process($container); } } diff --git a/src/Symfony/Component/Translation/Tests/Formatter/IntlFormatterTest.php b/src/Symfony/Component/Translation/Tests/Formatter/IntlFormatterTest.php index 4bf8ed43e8389..9e2d22752bfd5 100644 --- a/src/Symfony/Component/Translation/Tests/Formatter/IntlFormatterTest.php +++ b/src/Symfony/Component/Translation/Tests/Formatter/IntlFormatterTest.php @@ -90,12 +90,22 @@ public static function provideDataForFormat() ]; } - public function testPercentsAndBracketsAreTrimmed() + /** + * @dataProvider percentAndBracketsAreTrimmedProvider + */ + public function testPercentsAndBracketsAreTrimmed(string $expected, string $message, array $paramters) { $formatter = new IntlFormatter(); $this->assertInstanceof(IntlFormatterInterface::class, $formatter); - $this->assertSame('Hello Fab', $formatter->formatIntl('Hello {name}', 'en', ['name' => 'Fab'])); - $this->assertSame('Hello Fab', $formatter->formatIntl('Hello {name}', 'en', ['%name%' => 'Fab'])); - $this->assertSame('Hello Fab', $formatter->formatIntl('Hello {name}', 'en', ['{{ name }}' => 'Fab'])); + $this->assertSame($expected, $formatter->formatIntl($message, 'en', $paramters)); + } + + public static function percentAndBracketsAreTrimmedProvider(): array + { + return [ + ['Hello Fab', 'Hello {name}', ['name' => 'Fab']], + ['Hello Fab', 'Hello {name}', ['%name%' => 'Fab']], + ['Hello Fab', 'Hello {name}', ['{{ name }}' => 'Fab']], + ]; } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/CsvFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/CsvFileLoaderTest.php index 332d5a4d9330a..e43675ee9b773 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/CsvFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/CsvFileLoaderTest.php @@ -44,16 +44,14 @@ public function testLoadDoesNothingIfEmpty() public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); - $loader = new CsvFileLoader(); - $resource = __DIR__.'/../Fixtures/not-exists.csv'; - $loader->load($resource, 'en', 'domain1'); + + (new CsvFileLoader())->load(__DIR__.'/../Fixtures/not-exists.csv', 'en', 'domain1'); } public function testLoadNonLocalResource() { $this->expectException(InvalidResourceException::class); - $loader = new CsvFileLoader(); - $resource = 'http://example.com/resources.csv'; - $loader->load($resource, 'en', 'domain1'); + + (new CsvFileLoader())->load('http://example.com/resources.csv', 'en', 'domain1'); } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/IcuDatFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/IcuDatFileLoaderTest.php index fca84fa5bf8a5..15fe11bc16985 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/IcuDatFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/IcuDatFileLoaderTest.php @@ -24,8 +24,8 @@ class IcuDatFileLoaderTest extends LocalizedTestCase public function testLoadInvalidResource() { $this->expectException(InvalidResourceException::class); - $loader = new IcuDatFileLoader(); - $loader->load(__DIR__.'/../Fixtures/resourcebundle/corrupted/resources', 'es', 'domain2'); + + (new IcuDatFileLoader())->load(__DIR__.'/../Fixtures/resourcebundle/corrupted/resources', 'es', 'domain2'); } public function testDatEnglishLoad() @@ -56,7 +56,7 @@ public function testDatFrenchLoad() public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); - $loader = new IcuDatFileLoader(); - $loader->load(__DIR__.'/../Fixtures/non-existing.txt', 'en', 'domain1'); + + (new IcuDatFileLoader())->load(__DIR__.'/../Fixtures/non-existing.txt', 'en', 'domain1'); } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/IcuResFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/IcuResFileLoaderTest.php index 7bce83211ea1a..066c072ddc1c8 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/IcuResFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/IcuResFileLoaderTest.php @@ -36,14 +36,14 @@ public function testLoad() public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); - $loader = new IcuResFileLoader(); - $loader->load(__DIR__.'/../Fixtures/non-existing.txt', 'en', 'domain1'); + + (new IcuResFileLoader())->load(__DIR__.'/../Fixtures/non-existing.txt', 'en', 'domain1'); } public function testLoadInvalidResource() { $this->expectException(InvalidResourceException::class); - $loader = new IcuResFileLoader(); - $loader->load(__DIR__.'/../Fixtures/resourcebundle/corrupted', 'en', 'domain1'); + + (new IcuResFileLoader())->load(__DIR__.'/../Fixtures/resourcebundle/corrupted', 'en', 'domain1'); } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/IniFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/IniFileLoaderTest.php index cfac4903a7207..8617664991551 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/IniFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/IniFileLoaderTest.php @@ -43,8 +43,7 @@ public function testLoadDoesNothingIfEmpty() public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); - $loader = new IniFileLoader(); - $resource = __DIR__.'/../Fixtures/non-existing.ini'; - $loader->load($resource, 'en', 'domain1'); + + (new IniFileLoader())->load(__DIR__.'/../Fixtures/non-existing.ini', 'en', 'domain1'); } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/JsonFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/JsonFileLoaderTest.php index 54f08a741a75d..5160cfb74e8a1 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/JsonFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/JsonFileLoaderTest.php @@ -44,17 +44,15 @@ public function testLoadDoesNothingIfEmpty() public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); - $loader = new JsonFileLoader(); - $resource = __DIR__.'/../Fixtures/non-existing.json'; - $loader->load($resource, 'en', 'domain1'); + + (new JsonFileLoader())->load(__DIR__.'/../Fixtures/non-existing.json', 'en', 'domain1'); } public function testParseException() { $this->expectException(InvalidResourceException::class); $this->expectExceptionMessage('Error parsing JSON: Syntax error, malformed JSON'); - $loader = new JsonFileLoader(); - $resource = __DIR__.'/../Fixtures/malformed.json'; - $loader->load($resource, 'en', 'domain1'); + + (new JsonFileLoader())->load(__DIR__.'/../Fixtures/malformed.json', 'en', 'domain1'); } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/MoFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/MoFileLoaderTest.php index 562ea0e478d38..3c494a06dee9b 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/MoFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/MoFileLoaderTest.php @@ -47,17 +47,15 @@ public function testLoadPlurals() public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); - $loader = new MoFileLoader(); - $resource = __DIR__.'/../Fixtures/non-existing.mo'; - $loader->load($resource, 'en', 'domain1'); + + (new MoFileLoader())->load(__DIR__.'/../Fixtures/non-existing.mo', 'en', 'domain1'); } public function testLoadInvalidResource() { $this->expectException(InvalidResourceException::class); - $loader = new MoFileLoader(); - $resource = __DIR__.'/../Fixtures/empty.mo'; - $loader->load($resource, 'en', 'domain1'); + + (new MoFileLoader())->load(__DIR__.'/../Fixtures/empty.mo', 'en', 'domain1'); } public function testLoadEmptyTranslation() diff --git a/src/Symfony/Component/Translation/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/PhpFileLoaderTest.php index e5ae2e89fa068..ce76c15f5a899 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/PhpFileLoaderTest.php @@ -33,16 +33,14 @@ public function testLoad() public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); - $loader = new PhpFileLoader(); - $resource = __DIR__.'/../Fixtures/non-existing.php'; - $loader->load($resource, 'en', 'domain1'); + + (new PhpFileLoader())->load(__DIR__.'/../Fixtures/non-existing.php', 'en', 'domain1'); } public function testLoadThrowsAnExceptionIfFileNotLocal() { $this->expectException(InvalidResourceException::class); - $loader = new PhpFileLoader(); - $resource = 'http://example.com/resources.php'; - $loader->load($resource, 'en', 'domain1'); + + (new PhpFileLoader())->load('http://example.com/resources.php', 'en', 'domain1'); } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php index 4822de76cb0a8..3e963f68285cb 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php @@ -57,9 +57,8 @@ public function testLoadDoesNothingIfEmpty() public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); - $loader = new PoFileLoader(); - $resource = __DIR__.'/../Fixtures/non-existing.po'; - $loader->load($resource, 'en', 'domain1'); + + (new PoFileLoader())->load(__DIR__.'/../Fixtures/non-existing.po', 'en', 'domain1'); } public function testLoadEmptyTranslation() diff --git a/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php index 908dca31e8f77..72aa4bfaa9c27 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php @@ -37,35 +37,31 @@ public function testLoad() public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); - $loader = new QtFileLoader(); - $resource = __DIR__.'/../Fixtures/non-existing.ts'; - $loader->load($resource, 'en', 'domain1'); + + (new QtFileLoader())->load(__DIR__.'/../Fixtures/non-existing.ts', 'en', 'domain1'); } public function testLoadNonLocalResource() { $this->expectException(InvalidResourceException::class); - $loader = new QtFileLoader(); - $resource = 'http://domain1.com/resources.ts'; - $loader->load($resource, 'en', 'domain1'); + + (new QtFileLoader())->load('http://domain1.com/resources.ts', 'en', 'domain1'); } public function testLoadInvalidResource() { $this->expectException(InvalidResourceException::class); - $loader = new QtFileLoader(); - $resource = __DIR__.'/../Fixtures/invalid-xml-resources.xlf'; - $loader->load($resource, 'en', 'domain1'); + + (new QtFileLoader())->load(__DIR__.'/../Fixtures/invalid-xml-resources.xlf', 'en', 'domain1'); } public function testLoadEmptyResource() { - $loader = new QtFileLoader(); $resource = __DIR__.'/../Fixtures/empty.xlf'; $this->expectException(InvalidResourceException::class); $this->expectExceptionMessage(sprintf('Unable to load "%s".', $resource)); - $loader->load($resource, 'en', 'domain1'); + (new QtFileLoader())->load($resource, 'en', 'domain1'); } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php index b64b6f9511519..35442e59675d0 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php @@ -143,50 +143,47 @@ public function testTargetAttributesAreStoredCorrectly() public function testLoadInvalidResource() { $this->expectException(InvalidResourceException::class); - $loader = new XliffFileLoader(); - $loader->load(__DIR__.'/../Fixtures/resources.php', 'en', 'domain1'); + + (new XliffFileLoader())->load(__DIR__.'/../Fixtures/resources.php', 'en', 'domain1'); } public function testLoadResourceDoesNotValidate() { $this->expectException(InvalidResourceException::class); - $loader = new XliffFileLoader(); - $loader->load(__DIR__.'/../Fixtures/non-valid.xlf', 'en', 'domain1'); + + (new XliffFileLoader())->load(__DIR__.'/../Fixtures/non-valid.xlf', 'en', 'domain1'); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); - $loader = new XliffFileLoader(); - $resource = __DIR__.'/../Fixtures/non-existing.xlf'; - $loader->load($resource, 'en', 'domain1'); + + (new XliffFileLoader())->load(__DIR__.'/../Fixtures/non-existing.xlf', 'en', 'domain1'); } public function testLoadThrowsAnExceptionIfFileNotLocal() { $this->expectException(InvalidResourceException::class); - $loader = new XliffFileLoader(); - $resource = 'http://example.com/resources.xlf'; - $loader->load($resource, 'en', 'domain1'); + + (new XliffFileLoader())->load('http://example.com/resources.xlf', 'en', 'domain1'); } public function testDocTypeIsNotAllowed() { $this->expectException(InvalidResourceException::class); $this->expectExceptionMessage('Document types are not allowed.'); - $loader = new XliffFileLoader(); - $loader->load(__DIR__.'/../Fixtures/withdoctype.xlf', 'en', 'domain1'); + + (new XliffFileLoader())->load(__DIR__.'/../Fixtures/withdoctype.xlf', 'en', 'domain1'); } public function testParseEmptyFile() { - $loader = new XliffFileLoader(); $resource = __DIR__.'/../Fixtures/empty.xlf'; $this->expectException(InvalidResourceException::class); $this->expectExceptionMessage(sprintf('Unable to load "%s":', $resource)); - $loader->load($resource, 'en', 'domain1'); + (new XliffFileLoader())->load($resource, 'en', 'domain1'); } public function testLoadNotes() diff --git a/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php index 647cd3acc2c85..f5d67998e04e6 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php @@ -52,25 +52,31 @@ public function testLoadDoesNothingIfEmpty() public function testLoadNonExistingResource() { - $this->expectException(NotFoundResourceException::class); $loader = new YamlFileLoader(); $resource = __DIR__.'/../Fixtures/non-existing.yml'; + + $this->expectException(NotFoundResourceException::class); + $loader->load($resource, 'en', 'domain1'); } public function testLoadThrowsAnExceptionIfFileNotLocal() { - $this->expectException(InvalidResourceException::class); $loader = new YamlFileLoader(); $resource = 'http://example.com/resources.yml'; + + $this->expectException(InvalidResourceException::class); + $loader->load($resource, 'en', 'domain1'); } public function testLoadThrowsAnExceptionIfNotAnArray() { - $this->expectException(InvalidResourceException::class); $loader = new YamlFileLoader(); $resource = __DIR__.'/../Fixtures/non-valid.yml'; + + $this->expectException(InvalidResourceException::class); + $loader->load($resource, 'en', 'domain1'); } } diff --git a/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php b/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php index fb118e93afbb8..439e2ba045b22 100644 --- a/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php +++ b/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php @@ -191,30 +191,36 @@ public function testAddFallbackCatalogue() public function testAddFallbackCatalogueWithParentCircularReference() { - $this->expectException(LogicException::class); $main = new MessageCatalogue('en_US'); $fallback = new MessageCatalogue('fr_FR'); $fallback->addFallbackCatalogue($main); + + $this->expectException(LogicException::class); + $main->addFallbackCatalogue($fallback); } public function testAddFallbackCatalogueWithFallbackCircularReference() { - $this->expectException(LogicException::class); $fr = new MessageCatalogue('fr'); $en = new MessageCatalogue('en'); $es = new MessageCatalogue('es'); $fr->addFallbackCatalogue($en); $es->addFallbackCatalogue($en); + + $this->expectException(LogicException::class); + $en->addFallbackCatalogue($fr); } public function testAddCatalogueWhenLocaleIsNotTheSameAsTheCurrentOne() { - $this->expectException(LogicException::class); $catalogue = new MessageCatalogue('en'); + + $this->expectException(LogicException::class); + $catalogue->addCatalogue(new MessageCatalogue('fr', [])); } diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index 9ba54f6bf6763..ca7cb0d01e595 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -69,17 +69,19 @@ public function testSetGetLocale() /** * @dataProvider getInvalidLocalesTests */ - public function testSetInvalidLocale($locale) + public function testSetInvalidLocale(string $locale) { - $this->expectException(InvalidArgumentException::class); $translator = new Translator('fr'); + + $this->expectException(InvalidArgumentException::class); + $translator->setLocale($locale); } /** * @dataProvider getValidLocalesTests */ - public function testSetValidLocale($locale) + public function testSetValidLocale(string $locale) { $translator = new Translator($locale); $translator->setLocale($locale); @@ -186,15 +188,17 @@ public function testTransWithFallbackLocale() */ public function testAddResourceInvalidLocales($locale) { - $this->expectException(InvalidArgumentException::class); $translator = new Translator('fr'); + + $this->expectException(InvalidArgumentException::class); + $translator->addResource('array', ['foo' => 'foofoo'], $locale); } /** * @dataProvider getValidLocalesTests */ - public function testAddResourceValidLocales($locale) + public function testAddResourceValidLocales(string $locale) { $translator = new Translator('fr'); $translator->addResource('array', ['foo' => 'foofoo'], $locale); @@ -219,15 +223,16 @@ public function testAddResourceAfterTrans() /** * @dataProvider getTransFileTests */ - public function testTransWithoutFallbackLocaleFile($format, $loader) + public function testTransWithoutFallbackLocaleFile(string $format, string $loader) { - $this->expectException(NotFoundResourceException::class); $loaderClass = 'Symfony\\Component\\Translation\\Loader\\'.$loader; $translator = new Translator('en'); $translator->addLoader($format, new $loaderClass()); $translator->addResource($format, __DIR__.'/Fixtures/non-existing', 'en'); $translator->addResource($format, __DIR__.'/Fixtures/resources.'.$format, 'en'); + $this->expectException(NotFoundResourceException::class); + // force catalogue loading $translator->trans('foo'); } @@ -235,7 +240,7 @@ public function testTransWithoutFallbackLocaleFile($format, $loader) /** * @dataProvider getTransFileTests */ - public function testTransWithFallbackLocaleFile($format, $loader) + public function testTransWithFallbackLocaleFile(string $format, string $loader) { $loaderClass = 'Symfony\\Component\\Translation\\Loader\\'.$loader; $translator = new Translator('en_GB'); @@ -343,10 +348,11 @@ public function testTransNonExistentWithFallback() public function testWhenAResourceHasNoRegisteredLoader() { - $this->expectException(RuntimeException::class); $translator = new Translator('en'); $translator->addResource('array', ['foo' => 'foofoo'], 'en'); + $this->expectException(RuntimeException::class); + $translator->trans('foo'); } @@ -409,18 +415,19 @@ public function testTransICU(...$args) */ public function testTransInvalidLocale($locale) { - $this->expectException(InvalidArgumentException::class); $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foofoo'], 'en'); + $this->expectException(InvalidArgumentException::class); + $translator->trans('foo', [], '', $locale); } /** * @dataProvider getValidLocalesTests */ - public function testTransValidLocale($locale) + public function testTransValidLocale(string $locale) { $translator = new Translator($locale); $translator->addLoader('array', new ArrayLoader()); @@ -433,7 +440,7 @@ public function testTransValidLocale($locale) /** * @dataProvider getFlattenedTransTests */ - public function testFlattenedTrans($expected, $messages, $id) + public function testFlattenedTrans(string $expected, $messages, $id) { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); @@ -470,7 +477,7 @@ public static function getTransFileTests() ]; } - public static function getTransTests(): iterable + public static function getTransTests(): array { $param = new TranslatableMessage('Symfony is %what%!', ['%what%' => 'awesome'], ''); @@ -587,11 +594,12 @@ public function testIntlDomainOverlapseWithIntlResourceBefore() public function testMissingLoaderForResourceError() { + $translator = new Translator('en'); + $translator->addResource('twig', 'messages.en.twig', 'en'); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('No loader is registered for the "twig" format when loading the "messages.en.twig" resource.'); - $translator = new Translator('en'); - $translator->addResource('twig', 'messages.en.twig', 'en'); $translator->getCatalogue('en'); } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php b/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php index 521d5abd8e3f9..0b3a3897dbebb 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php @@ -231,11 +231,9 @@ public static function throwsOnInvalidStringDatesProvider(): array 'value' => 'foo', ]); - $constraintClass = $constraint::class; - return [ - [$constraint, sprintf('The compared value "foo" could not be converted to a "DateTimeImmutable" instance in the "%s" constraint.', $constraintClass), new \DateTimeImmutable()], - [$constraint, sprintf('The compared value "foo" could not be converted to a "DateTime" instance in the "%s" constraint.', $constraintClass), new \DateTime()], + [$constraint, sprintf('The compared value "foo" could not be converted to a "DateTimeImmutable" instance in the "%s" constraint.', $constraint::class), new \DateTimeImmutable()], + [$constraint, sprintf('The compared value "foo" could not be converted to a "DateTime" instance in the "%s" constraint.', $constraint::class), new \DateTime()], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php index 5c7904a8001af..081d41a57c705 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php @@ -369,7 +369,12 @@ public function testInvalidRGBA($cssColor) public static function getInvalidRGBA(): array { - return [['rgba(999,999,999,999)'], ['rgba(-99,-99,-99,-99)'], ['rgba(a,b,c,d)'], ['rgba(99 99, 9 99, 99 9, . 9)']]; + return [ + ['rgba(999,999,999,999)'], + ['rgba(-99,-99,-99,-99)'], + ['rgba(a,b,c,d)'], + ['rgba(99 99, 9 99, 99 9, . 9)'], + ]; } /** @@ -415,7 +420,13 @@ public function testInvalidHSLA($cssColor) public function getInvalidHSLA(): array { - return [['hsla(1000, 1000%, 20000%, 999)'], ['hsla(-100, -10%, -2%, 999)'], ['hsla(a, b, c, d)'], ['hsla(a, b%, c%, d)'], ['hsla( 9 99% , 99 9% , 9 %']]; + return [ + ['hsla(1000, 1000%, 20000%, 999)'], + ['hsla(-100, -10%, -2%, 999)'], + ['hsla(a, b, c, d)'], + ['hsla(a, b%, c%, d)'], + ['hsla( 9 99% , 99 9% , 9 %'], + ]; } /** diff --git a/src/Symfony/Component/Validator/Tests/Constraints/EmailValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/EmailValidatorTest.php index d894236e104af..ab424a82d25d6 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/EmailValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/EmailValidatorTest.php @@ -245,11 +245,12 @@ public function testModeHtml5AllowNoTld() public function testUnknownModesOnValidateTriggerException() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Email::$mode" parameter value is not valid.'); $constraint = new Email(); $constraint->mode = 'Unknown Mode'; + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Email::$mode" parameter value is not valid.'); + $this->validator->validate('example@example..com', $constraint); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileTest.php index b05a3ec86aafa..e8c27b4b1f290 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/FileTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileTest.php @@ -57,8 +57,10 @@ public function testMaxSizeCanBeSetAfterInitialization($maxSize, $bytes, $binary */ public function testInvalidValueForMaxSizeThrowsExceptionAfterInitialization($maxSize) { - $this->expectException(ConstraintDefinitionException::class); $file = new File(['maxSize' => 1000]); + + $this->expectException(ConstraintDefinitionException::class); + $file->maxSize = $maxSize; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTestCase.php b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTestCase.php index 960a8f3b6e2f3..b5d05801e53fe 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTestCase.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTestCase.php @@ -519,10 +519,11 @@ public static function uploadedFileErrorProvider() public function testNegativeMaxSize() { + $file = new File(); + $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage('"-1" is not a valid maximum size.'); - $file = new File(); $file->maxSize = -1; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php index 34e546029c731..11b092c6df484 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php @@ -69,15 +69,11 @@ public function testThrowsConstraintExceptionIfValue() */ public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options) { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires either the "value" or "propertyPath" option to be set.'); - $this->markTestSkipped('Value option always set for PositiveOrZero constraint'); + $this->markTestSkipped('Value option always set for PositiveOrZero constraint'); } public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires only one of the "value" or "propertyPath" options to be set, not both.'); $this->markTestSkipped('Value option is set for PositiveOrZero constraint automatically'); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php index 5ce59d129cf80..18a503bf237d5 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php @@ -67,15 +67,11 @@ public function testThrowsConstraintExceptionIfValue() */ public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options) { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires either the "value" or "propertyPath" option to be set.'); $this->markTestSkipped('Value option always set for Positive constraint.'); } public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires only one of the "value" or "propertyPath" options to be set, not both.'); $this->markTestSkipped('Value option is set for Positive constraint automatically'); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php index 000b9d900c690..0afc9e6e15d64 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php @@ -116,7 +116,7 @@ public static function getThreeCharactersWithWhitespaces() /** * @dataProvider getFiveOrMoreCharacters */ - public function testValidValuesMin($value) + public function testValidValuesMin(int|string $value) { $constraint = new Length(['min' => 5]); $this->validator->validate($value, $constraint); @@ -127,7 +127,7 @@ public function testValidValuesMin($value) /** * @dataProvider getThreeOrLessCharacters */ - public function testValidValuesMax($value) + public function testValidValuesMax(int|string $value) { $constraint = new Length(['max' => 3]); $this->validator->validate($value, $constraint); @@ -138,7 +138,7 @@ public function testValidValuesMax($value) /** * @dataProvider getFourCharacters */ - public function testValidValuesExact($value) + public function testValidValuesExact(int|string $value) { $constraint = new Length(4); $this->validator->validate($value, $constraint); @@ -184,7 +184,7 @@ public function testValidBytesValues() /** * @dataProvider getThreeOrLessCharacters */ - public function testInvalidValuesMin($value, $valueLength) + public function testInvalidValuesMin(int|string $value, int $valueLength) { $constraint = new Length([ 'min' => 4, @@ -206,7 +206,7 @@ public function testInvalidValuesMin($value, $valueLength) /** * @dataProvider getThreeOrLessCharacters */ - public function testInvalidValuesMinNamed($value, $valueLength) + public function testInvalidValuesMinNamed(int|string $value, int $valueLength) { $constraint = new Length(min: 4, minMessage: 'myMessage'); @@ -225,7 +225,7 @@ public function testInvalidValuesMinNamed($value, $valueLength) /** * @dataProvider getFiveOrMoreCharacters */ - public function testInvalidValuesMax($value, $valueLength) + public function testInvalidValuesMax(int|string $value, int $valueLength) { $constraint = new Length([ 'max' => 4, @@ -247,7 +247,7 @@ public function testInvalidValuesMax($value, $valueLength) /** * @dataProvider getFiveOrMoreCharacters */ - public function testInvalidValuesMaxNamed($value, $valueLength) + public function testInvalidValuesMaxNamed(int|string $value, int $valueLength) { $constraint = new Length(max: 4, maxMessage: 'myMessage'); @@ -266,7 +266,7 @@ public function testInvalidValuesMaxNamed($value, $valueLength) /** * @dataProvider getThreeOrLessCharacters */ - public function testInvalidValuesExactLessThanFour($value, $valueLength) + public function testInvalidValuesExactLessThanFour(int|string $value, int $valueLength) { $constraint = new Length([ 'min' => 4, @@ -289,7 +289,7 @@ public function testInvalidValuesExactLessThanFour($value, $valueLength) /** * @dataProvider getThreeOrLessCharacters */ - public function testInvalidValuesExactLessThanFourNamed($value, $valueLength) + public function testInvalidValuesExactLessThanFourNamed(int|string $value, int $valueLength) { $constraint = new Length(exactly: 4, exactMessage: 'myMessage'); @@ -308,7 +308,7 @@ public function testInvalidValuesExactLessThanFourNamed($value, $valueLength) /** * @dataProvider getFiveOrMoreCharacters */ - public function testInvalidValuesExactMoreThanFour($value, $valueLength) + public function testInvalidValuesExactMoreThanFour(int|string $value, int $valueLength) { $constraint = new Length([ 'min' => 4, diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php index f75364e1dc359..946bf93c7826b 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php @@ -67,15 +67,11 @@ public function testThrowsConstraintExceptionIfValue() */ public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options) { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires either the "value" or "propertyPath" option to be set.'); $this->markTestSkipped('Value option always set for NegativeOrZero constraint'); } public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires only one of the "value" or "propertyPath" options to be set, not both.'); $this->markTestSkipped('Value option is set for NegativeOrZero constraint automatically'); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php index 569e662b2be9b..35fac73ecab34 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php @@ -67,15 +67,11 @@ public function testThrowsConstraintExceptionIfValue() */ public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options) { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires either the "value" or "propertyPath" option to be set.'); $this->markTestSkipped('Value option always set for Negative constraint'); } public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires only one of the "value" or "propertyPath" options to be set, not both.'); $this->markTestSkipped('Value option is set for Negative constraint automatically'); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php index 4eec91c63d683..1626eecef48ff 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php @@ -151,7 +151,7 @@ public function testInvalidLocaleWithoutCanonicalizationNamed() ->assertRaised(); } - public static function getUncanonicalizedLocales(): iterable + public static function getUncanonicalizedLocales(): array { return [ ['en-US'], diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php index 7c2e587cf6b89..b5084b6d7f05f 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php @@ -44,9 +44,10 @@ public function testEmptyStringIsValid() public function testExpectsUuidConstraintCompatibleType() { - $this->expectException(UnexpectedTypeException::class); $constraint = $this->getMockForAbstractClass(Constraint::class); + $this->expectException(UnexpectedTypeException::class); + $this->validator->validate('216fff40-98d9-11e3-a5e2-0800200c9a66', $constraint); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/WhenTest.php b/src/Symfony/Component/Validator/Tests/Constraints/WhenTest.php index 12d2bd146dda1..7cfe13f2f2e78 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/WhenTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/WhenTest.php @@ -36,9 +36,7 @@ public function testNonConstraintsAreRejected() { $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage('The value "foo" is not an instance of Constraint in constraint "Symfony\Component\Validator\Constraints\When"'); - new When('true', [ - 'foo', - ]); + new When('true', ['foo']); } public function testAttributes() diff --git a/src/Symfony/Component/Validator/Tests/ContainerConstraintValidatorFactoryTest.php b/src/Symfony/Component/Validator/Tests/ContainerConstraintValidatorFactoryTest.php index 63b7f6f96ae01..980a70e40a66c 100644 --- a/src/Symfony/Component/Validator/Tests/ContainerConstraintValidatorFactoryTest.php +++ b/src/Symfony/Component/Validator/Tests/ContainerConstraintValidatorFactoryTest.php @@ -49,7 +49,6 @@ public function testGetInstanceReturnsService() public function testGetInstanceInvalidValidatorClass() { - $this->expectException(ValidatorException::class); $constraint = $this->createMock(Constraint::class); $constraint ->expects($this->once()) @@ -57,6 +56,9 @@ public function testGetInstanceInvalidValidatorClass() ->willReturn('Fully\\Qualified\\ConstraintValidator\\Class\\Name'); $factory = new ContainerConstraintValidatorFactory(new Container()); + + $this->expectException(ValidatorException::class); + $factory->getInstance($constraint); } } diff --git a/src/Symfony/Component/Validator/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php b/src/Symfony/Component/Validator/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php index 052c88f85319b..1b85c7e2a37ff 100644 --- a/src/Symfony/Component/Validator/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php +++ b/src/Symfony/Component/Validator/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php @@ -47,8 +47,6 @@ public function testThatConstraintValidatorServicesAreProcessed() public function testAbstractConstraintValidator() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The service "my_abstract_constraint_validator" tagged "validator.constraint_validator" must not be abstract.'); $container = new ContainerBuilder(); $container->register('validator.validator_factory') ->addArgument([]); @@ -58,6 +56,10 @@ public function testAbstractConstraintValidator() ->addTag('validator.constraint_validator'); $addConstraintValidatorsPass = new AddConstraintValidatorsPass(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The service "my_abstract_constraint_validator" tagged "validator.constraint_validator" must not be abstract.'); + $addConstraintValidatorsPass->process($container); } diff --git a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php index 815f4a1f56680..d38ecf8605d1f 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php @@ -279,17 +279,21 @@ public function testGroupSequencesFailIfContainingDefault() public function testGroupSequenceFailsIfGroupSequenceProviderIsSet() { - $this->expectException(GroupDefinitionException::class); $metadata = new ClassMetadata(self::PROVIDERCLASS); $metadata->setGroupSequenceProvider(true); + + $this->expectException(GroupDefinitionException::class); + $metadata->setGroupSequence(['GroupSequenceProviderEntity', 'Foo']); } public function testGroupSequenceProviderFailsIfGroupSequenceIsSet() { - $this->expectException(GroupDefinitionException::class); $metadata = new ClassMetadata(self::PROVIDERCLASS); $metadata->setGroupSequence(['GroupSequenceProviderEntity', 'Foo']); + + $this->expectException(GroupDefinitionException::class); + $metadata->setGroupSequenceProvider(true); } diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php index 549bc518b41b5..1fff113011620 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php @@ -20,14 +20,11 @@ class BlackHoleMetadataFactoryTest extends TestCase public function testGetMetadataForThrowsALogicException() { $this->expectException(LogicException::class); - $metadataFactory = new BlackHoleMetadataFactory(); - $metadataFactory->getMetadataFor('foo'); + (new BlackHoleMetadataFactory())->getMetadataFor('foo'); } public function testHasMetadataForReturnsFalse() { - $metadataFactory = new BlackHoleMetadataFactory(); - - $this->assertFalse($metadataFactory->hasMetadataFor('foo')); + $this->assertFalse((new BlackHoleMetadataFactory())->hasMetadataFor('foo')); } } diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php index 3d10506aea337..d2250114ffbff 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php @@ -109,14 +109,17 @@ public function testCachedMetadata() public function testNonClassNameStringValues() { - $this->expectException(NoSuchMetadataException::class); $testedValue = 'error@example.com'; $loader = $this->createMock(LoaderInterface::class); $cache = $this->createMock(CacheItemPoolInterface::class); - $factory = new LazyLoadingMetadataFactory($loader, $cache); $cache ->expects($this->never()) ->method('getItem'); + + $factory = new LazyLoadingMetadataFactory($loader, $cache); + + $this->expectException(NoSuchMetadataException::class); + $factory->getMetadataFor($testedValue); } diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php index a5c983939bcb2..60493787e1ba5 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -48,10 +48,11 @@ public function testLoadClassMetadataReturnsFalseIfEmpty() */ public function testInvalidYamlFiles($path) { - $this->expectException(\InvalidArgumentException::class); $loader = new YamlFileLoader(__DIR__.'/'.$path); $metadata = new ClassMetadata(Entity::class); + $this->expectException(\InvalidArgumentException::class); + $loader->loadClassMetadata($metadata); } diff --git a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php index cffbaa5fbeca5..ee183a1bfdf15 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php @@ -531,12 +531,13 @@ public function testsIgnoreNullReference() public function testFailOnScalarReferences() { - $this->expectException(NoSuchMetadataException::class); $entity = new Entity(); $entity->reference = 'string'; $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->expectException(NoSuchMetadataException::class); + $this->validate($entity); } @@ -786,7 +787,6 @@ public function testDisableTraversableTraversal() public function testMetadataMustExistIfTraversalIsDisabled() { - $this->expectException(NoSuchMetadataException::class); $entity = new Entity(); $entity->reference = new \ArrayIterator(); @@ -794,6 +794,8 @@ public function testMetadataMustExistIfTraversalIsDisabled() 'traverse' => false, ])); + $this->expectException(NoSuchMetadataException::class); + $this->validate($entity); } @@ -1670,12 +1672,11 @@ public function testTraversalDisabledOnClass() public function testExpectTraversableIfTraversalEnabledOnClass() { - $this->expectException(ConstraintDefinitionException::class); - $entity = new Entity(); - $this->metadata->addConstraint(new Traverse(true)); - $this->validator->validate($entity); + $this->expectException(ConstraintDefinitionException::class); + + $this->validator->validate(new Entity()); } public function testReferenceTraversalDisabledOnClass() diff --git a/src/Symfony/Component/Webhook/Tests/Client/RequestParserTest.php b/src/Symfony/Component/Webhook/Tests/Client/RequestParserTest.php index 18dbe5c1ff616..53171866d6e47 100644 --- a/src/Symfony/Component/Webhook/Tests/Client/RequestParserTest.php +++ b/src/Symfony/Component/Webhook/Tests/Client/RequestParserTest.php @@ -21,9 +21,6 @@ class RequestParserTest extends TestCase public function testParseDoesNotMatch() { $this->expectException(RejectWebhookException::class); - - $request = new Request(); - $parser = new RequestParser(); - $parser->parse($request, '$ecret'); + (new RequestParser())->parse(new Request(), '$ecret'); } } diff --git a/src/Symfony/Component/Workflow/Tests/DefinitionTest.php b/src/Symfony/Component/Workflow/Tests/DefinitionTest.php index 9e9c7832f4a1e..3dc40dd5634de 100644 --- a/src/Symfony/Component/Workflow/Tests/DefinitionTest.php +++ b/src/Symfony/Component/Workflow/Tests/DefinitionTest.php @@ -64,18 +64,20 @@ public function testAddTransition() public function testAddTransitionAndFromPlaceIsNotDefined() { + $places = range('a', 'b'); + $this->expectException(LogicException::class); $this->expectExceptionMessage('Place "c" referenced in transition "name" does not exist.'); - $places = range('a', 'b'); new Definition($places, [new Transition('name', 'c', $places[1])]); } public function testAddTransitionAndToPlaceIsNotDefined() { + $places = range('a', 'b'); + $this->expectException(LogicException::class); $this->expectExceptionMessage('Place "c" referenced in transition "name" does not exist.'); - $places = range('a', 'b'); new Definition($places, [new Transition('name', $places[0], 'c')]); } diff --git a/src/Symfony/Component/Workflow/Tests/RegistryTest.php b/src/Symfony/Component/Workflow/Tests/RegistryTest.php index f9a8fe0200318..d3282a8bce060 100644 --- a/src/Symfony/Component/Workflow/Tests/RegistryTest.php +++ b/src/Symfony/Component/Workflow/Tests/RegistryTest.php @@ -63,18 +63,14 @@ public function testGetWithMultipleMatch() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Too many workflows (workflow2, workflow3) match this subject (Symfony\Component\Workflow\Tests\Subject2); set a different name on each and use the second (name) argument of this method.'); - $w1 = $this->registry->get(new Subject2()); - $this->assertInstanceOf(Workflow::class, $w1); - $this->assertSame('workflow1', $w1->getName()); + $this->registry->get(new Subject2()); } public function testGetWithNoMatch() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Unable to find a workflow for class "stdClass".'); - $w1 = $this->registry->get(new \stdClass()); - $this->assertInstanceOf(Workflow::class, $w1); - $this->assertSame('workflow1', $w1->getName()); + $this->registry->get(new \stdClass()); } public function testAllWithOneMatchWithSuccess() diff --git a/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php b/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php index 036ece77f442d..34eeda6f82721 100644 --- a/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php +++ b/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php @@ -24,8 +24,6 @@ class WorkflowValidatorTest extends TestCase public function testWorkflowWithInvalidNames() { - $this->expectException(InvalidDefinitionException::class); - $this->expectExceptionMessage('All transitions for a place must have an unique name. Multiple transitions named "t1" where found for place "a" in workflow "foo".'); $places = range('a', 'c'); $transitions = []; @@ -35,6 +33,9 @@ public function testWorkflowWithInvalidNames() $definition = new Definition($places, $transitions); + $this->expectException(InvalidDefinitionException::class); + $this->expectExceptionMessage('All transitions for a place must have an unique name. Multiple transitions named "t1" where found for place "a" in workflow "foo".'); + (new WorkflowValidator())->validate($definition, 'foo'); } diff --git a/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php b/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php index 5208f123da871..a501f48d09e37 100644 --- a/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php +++ b/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php @@ -143,11 +143,12 @@ public function testLintWithExclude() public function testLintFileNotReadable() { - $this->expectException(\RuntimeException::class); $tester = $this->createCommandTester(); $filename = $this->createFile(''); unlink($filename); + $this->expectException(\RuntimeException::class); + $tester->execute(['filename' => $filename], ['decorated' => false]); } diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index 62fbb6af41b34..36b93e967da48 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -827,20 +827,20 @@ public function testTheEmptyStringIsAValidMappingKey() /** * @dataProvider getNotPhpCompatibleMappingKeyData */ - public function testImplicitStringCastingOfMappingKeysThrows($yaml, $expected) + public function testImplicitStringCastingOfMappingKeysThrowsException(string $yaml) { $this->expectException(ParseException::class); $this->expectExceptionMessage('Implicit casting of incompatible mapping keys to strings is not supported. Quote your evaluable mapping keys instead'); - $this->assertSame($expected, Inline::parse($yaml)); + Inline::parse($yaml); } public static function getNotPhpCompatibleMappingKeyData() { return [ - 'boolean-true' => ['{true: "foo"}', ['true' => 'foo']], - 'boolean-false' => ['{false: "foo"}', ['false' => 'foo']], - 'null' => ['{null: "foo"}', ['null' => 'foo']], - 'float' => ['{0.25: "foo"}', ['0.25' => 'foo']], + 'boolean-true' => ['{true: "foo"}'], + 'boolean-false' => ['{false: "foo"}'], + 'null' => ['{null: "foo"}'], + 'float' => ['{0.25: "foo"}'], ]; } diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index c34af87388b7f..9abdf5b02533d 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -650,25 +650,27 @@ public static function getObjectForMapTests() public function testObjectsSupportDisabledWithExceptions() { - $this->expectException(ParseException::class); $yaml = <<<'EOF' foo: !php/object:O:30:"Symfony\Tests\Component\Yaml\B":1:{s:1:"b";s:3:"foo";} bar: 1 EOF; + $this->expectException(ParseException::class); + $this->parser->parse($yaml, Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE); } public function testMappingKeyInMultiLineStringThrowsException() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Mapping values are not allowed in multi-line blocks at line 2 (near "dbal:wrong").'); - $yaml = <<<'EOF' data: dbal:wrong default_connection: monolith EOF; + + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Mapping values are not allowed in multi-line blocks at line 2 (near "dbal:wrong").'); + $this->parser->parse($yaml); } @@ -707,7 +709,6 @@ public function testNonUtf8Exception() public function testUnindentedCollectionException() { - $this->expectException(ParseException::class); $yaml = <<<'EOF' collection: @@ -717,12 +718,13 @@ public function testUnindentedCollectionException() EOF; + $this->expectException(ParseException::class); + $this->parser->parse($yaml); } public function testShortcutKeyUnindentedCollectionException() { - $this->expectException(ParseException::class); $yaml = <<<'EOF' collection: @@ -731,6 +733,8 @@ public function testShortcutKeyUnindentedCollectionException() EOF; + $this->expectException(ParseException::class); + $this->parser->parse($yaml); } @@ -929,8 +933,6 @@ public function testScalarInSequence() */ public function testMappingDuplicateKeyBlock() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Duplicate key "child" detected'); $input = <<<'EOD' parent: child: first @@ -939,28 +941,24 @@ public function testMappingDuplicateKeyBlock() child: duplicate child: duplicate EOD; - $expected = [ - 'parent' => [ - 'child' => 'first', - ], - ]; - $this->assertSame($expected, Yaml::parse($input)); + + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Duplicate key "child" detected'); + + Yaml::parse($input); } public function testMappingDuplicateKeyFlow() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Duplicate key "child" detected'); $input = <<<'EOD' parent: { child: first, child: duplicate } parent: { child: duplicate, child: duplicate } EOD; - $expected = [ - 'parent' => [ - 'child' => 'first', - ], - ]; - $this->assertSame($expected, Yaml::parse($input)); + + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Duplicate key "child" detected'); + + Yaml::parse($input); } /** @@ -1202,26 +1200,28 @@ public function testYamlDirective() public function testFloatKeys() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Numeric keys are not supported. Quote your evaluable mapping keys instead'); $yaml = <<<'EOF' foo: 1.2: "bar" 1.3: "baz" EOF; + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Numeric keys are not supported. Quote your evaluable mapping keys instead'); + $this->parser->parse($yaml); } public function testBooleanKeys() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Non-string keys are not supported. Quote your evaluable mapping keys instead'); $yaml = <<<'EOF' true: foo false: bar EOF; + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Non-string keys are not supported. Quote your evaluable mapping keys instead'); + $this->parser->parse($yaml); } @@ -1252,12 +1252,13 @@ public function testExplicitStringCasting() public function testColonInMappingValueException() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('A colon cannot be used in an unquoted mapping value'); $yaml = <<<'EOF' foo: bar: baz EOF; + $this->expectException(ParseException::class); + $this->expectExceptionMessage('A colon cannot be used in an unquoted mapping value'); + $this->parser->parse($yaml); } @@ -2347,21 +2348,20 @@ public function testExceptionWhenUsingUnsupportedBuiltInTags() public function testComplexMappingThrowsParseException() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Complex mappings are not supported at line 1 (near "? "1"").'); $yaml = <<expectException(ParseException::class); + $this->expectExceptionMessage('Complex mappings are not supported at line 1 (near "? "1"").'); + $this->parser->parse($yaml); } public function testComplexMappingNestedInMappingThrowsParseException() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Complex mappings are not supported at line 2 (near "? "1"").'); $yaml = <<expectException(ParseException::class); + $this->expectExceptionMessage('Complex mappings are not supported at line 2 (near "? "1"").'); + $this->parser->parse($yaml); } public function testComplexMappingNestedInSequenceThrowsParseException() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Complex mappings are not supported at line 1 (near "- ? "1"").'); $yaml = <<expectException(ParseException::class); + $this->expectExceptionMessage('Complex mappings are not supported at line 1 (near "- ? "1"").'); + $this->parser->parse($yaml); } public function testParsingIniThrowsException() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Unable to parse at line 2 (near " foo = bar").'); $ini = <<expectException(ParseException::class); + $this->expectExceptionMessage('Unable to parse at line 2 (near " foo = bar").'); + $this->parser->parse($ini); } @@ -2440,8 +2445,6 @@ public function testCanParseVeryLongValue() public function testParserCleansUpReferencesBetweenRuns() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Reference "foo" does not exist at line 2'); $yaml = <<expectException(ParseException::class); + $this->expectExceptionMessage('Reference "foo" does not exist at line 2'); + $this->parser->parse($yaml); } @@ -2574,8 +2581,6 @@ public function testParsingNonExistentFilesThrowsException() public function testParsingNotReadableFilesThrowsException() { - $this->expectException(ParseException::class); - $this->expectExceptionMessageMatches('#^File ".+/Fixtures/not_readable.yml" cannot be read\.$#'); if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('chmod is not supported on Windows'); } @@ -2587,6 +2592,9 @@ public function testParsingNotReadableFilesThrowsException() $file = __DIR__.'/Fixtures/not_readable.yml'; chmod($file, 0200); + $this->expectException(ParseException::class); + $this->expectExceptionMessageMatches('#^File ".+/Fixtures/not_readable.yml" cannot be read\.$#'); + $this->parser->parseFile($file); } @@ -2648,11 +2656,13 @@ public function testParseReferencesOnMergeKeysWithMappingsParsedAsObjects() public function testEvalRefException() { - $this->expectException(ParseException::class); - $this->expectExceptionMessage('Reference "foo" does not exist'); $yaml = <<expectException(ParseException::class); + $this->expectExceptionMessage('Reference "foo" does not exist'); + $this->parser->parse($yaml); } diff --git a/src/Symfony/Contracts/Service/Test/ServiceLocatorTestCase.php b/src/Symfony/Contracts/Service/Test/ServiceLocatorTestCase.php index 583f72a78ee22..65a3fe3379e93 100644 --- a/src/Symfony/Contracts/Service/Test/ServiceLocatorTestCase.php +++ b/src/Symfony/Contracts/Service/Test/ServiceLocatorTestCase.php @@ -12,7 +12,9 @@ namespace Symfony\Contracts\Service\Test; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use Symfony\Contracts\Service\ServiceLocatorTrait; abstract class ServiceLocatorTestCase extends TestCase @@ -66,27 +68,29 @@ public function testGetDoesNotMemoize() public function testThrowsOnUndefinedInternalService() { - if (!$this->getExpectedException()) { - $this->expectException(\Psr\Container\NotFoundExceptionInterface::class); - $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); - } $locator = $this->getServiceLocator([ 'foo' => function () use (&$locator) { return $locator->get('bar'); }, ]); + if (!$this->getExpectedException()) { + $this->expectException(NotFoundExceptionInterface::class); + $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); + } + $locator->get('foo'); } public function testThrowsOnCircularReference() { - $this->expectException(\Psr\Container\ContainerExceptionInterface::class); - $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); $locator = $this->getServiceLocator([ 'foo' => function () use (&$locator) { return $locator->get('bar'); }, 'bar' => function () use (&$locator) { return $locator->get('baz'); }, 'baz' => function () use (&$locator) { return $locator->get('bar'); }, ]); + $this->expectException(ContainerExceptionInterface::class); + $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); + $locator->get('foo'); } } diff --git a/src/Symfony/Contracts/Translation/Test/TranslatorTest.php b/src/Symfony/Contracts/Translation/Test/TranslatorTest.php index 18e669077713a..756228af548a3 100644 --- a/src/Symfony/Contracts/Translation/Test/TranslatorTest.php +++ b/src/Symfony/Contracts/Translation/Test/TranslatorTest.php @@ -183,9 +183,10 @@ public function testReturnMessageIfExactlyOneStandardRuleIsGiven() */ public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number) { - $this->expectException(\InvalidArgumentException::class); $translator = $this->getTranslator(); + $this->expectException(\InvalidArgumentException::class); + $translator->trans($id, ['%count%' => $number]); } From df88f47865c9c1c3f17f85329fd4cba748eb63e1 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 26 Dec 2023 09:30:13 +0100 Subject: [PATCH 072/395] [Notifier] Refactor Smsbox bridge --- .../Notifier/Bridge/Smsbox/Enum/Charset.php | 19 + .../Notifier/Bridge/Smsbox/Enum/Day.php | 28 ++ .../Notifier/Bridge/Smsbox/Enum/Encoding.php | 19 + .../Notifier/Bridge/Smsbox/Enum/Mode.php | 19 + .../Notifier/Bridge/Smsbox/Enum/Strategy.php | 20 + .../Notifier/Bridge/Smsbox/Enum/Udh.php | 19 + .../Notifier/Bridge/Smsbox/README.md | 24 +- .../Notifier/Bridge/Smsbox/SmsboxOptions.php | 375 ++++++------------ .../Bridge/Smsbox/SmsboxTransport.php | 64 ++- .../Bridge/Smsbox/SmsboxTransportFactory.php | 12 +- .../Bridge/Smsbox/Tests/SmsboxOptionsTest.php | 161 +++++++- .../Smsbox/Tests/SmsboxTransportTest.php | 86 ++-- .../Notifier/Bridge/Smsbox/composer.json | 3 +- 13 files changed, 490 insertions(+), 359 deletions(-) create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Charset.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Day.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Encoding.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Mode.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Strategy.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Udh.php diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Charset.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Charset.php new file mode 100644 index 0000000000000..3f102f3dcd78a --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Charset.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox\Enum; + +enum Charset: string +{ + case Iso1 = 'iso-8859-1'; + case Iso15 = 'iso-8859-15'; + case Utf8 = 'utf-8'; +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Day.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Day.php new file mode 100644 index 0000000000000..2c838cd5eb611 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Day.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox\Enum; + +enum Day: int +{ + case Monday = 1; + case Tuesday = 2; + case Wednesday = 3; + case Thursday = 4; + case Friday = 5; + case Saturday = 6; + case Sunday = 7; + + public function isBeforeOrEqualTo(Day $day): bool + { + return $this->value < $day->value; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Encoding.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Encoding.php new file mode 100644 index 0000000000000..9ae40d75c0f4b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Encoding.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox\Enum; + +enum Encoding: string +{ + case Default = 'default'; + case Unicode = 'unicode'; + case Auto = 'auto'; +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Mode.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Mode.php new file mode 100644 index 0000000000000..48d8e4d30714d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Mode.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox\Enum; + +enum Mode: string +{ + case Standard = 'Standard'; + case Expert = 'Expert'; + case Response = 'Reponse'; +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Strategy.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Strategy.php new file mode 100644 index 0000000000000..52b94487e7dcb --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Strategy.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox\Enum; + +enum Strategy: int +{ + case Private = 1; + case Notification = 2; + case NotMarketingGroup = 3; + case Marketing = 4; +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Udh.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Udh.php new file mode 100644 index 0000000000000..1d45483551d22 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Enum/Udh.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Smsbox\Enum; + +enum Udh: int +{ + case DisabledConcat = 0; + case SixBytes = 1; + case SevenBytes = 2; +} diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/README.md b/src/Symfony/Component/Notifier/Bridge/Smsbox/README.md index 7f9b36691c754..cb0dc35f46710 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsbox/README.md +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/README.md @@ -22,29 +22,35 @@ where: With a SMSBOX Message, you can use the SmsboxOptions class and use the setters to add [message options](https://www.smsbox.net/en/tools-development#developer-space) ```php -use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Charset; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Day; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Encoding; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Mode; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Strategy; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Udh; use Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions; +use Symfony\Component\Notifier\Message\SmsMessage; $sms = new SmsMessage('+33123456789', 'Your %1% message %2%'); $options = (new SmsboxOptions()) - ->mode(SmsboxOptions::MESSAGE_MODE_EXPERT) - ->strategy(SmsboxOptions::MESSAGE_STRATEGY_NOT_MARKETING_GROUP) + ->mode(Mode::Expert) + ->strategy(Strategy::NotMarketingGroup) ->sender('Your sender') ->date('DD/MM/YYYY') ->hour('HH:MM') - ->coding(SmsboxOptions::MESSAGE_CODING_UNICODE) - ->charset(SmsboxOptions::MESSAGE_CHARSET_UTF8) - ->udh(SmsboxOptions::MESSAGE_UDH_DISABLED_CONCAT) + ->coding(Encoding::Unicode) + ->charset(Charset::Iso1) + ->udh(Udh::DisabledConcat) ->callback(true) ->allowVocal(true) ->maxParts(2) ->validity(100) - ->daysMinMax(min: SmsboxOptions::MESSAGE_DAYS_TUESDAY, max: SmsboxOptions::MESSAGE_DAYS_FRIDAY) - ->hoursMinMax(min: 8, max: 10) + ->daysMinMax(min: Day::Tuesday, max: Day::Friday) + ->hoursMinMax(min: 8, max: 10) ->variable(['variable1', 'variable2']) ->dateTime(new \DateTime()) ->destIso('FR'); $sms->options($options); $texter->send($sms); -``` \ No newline at end of file +``` diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php index 031509587aed6..322dfd64c4a96 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxOptions.php @@ -11,49 +11,25 @@ namespace Symfony\Component\Notifier\Bridge\Smsbox; +use Symfony\Component\Intl\Countries; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Charset; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Day; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Encoding; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Mode; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Strategy; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Udh; use Symfony\Component\Notifier\Exception\InvalidArgumentException; use Symfony\Component\Notifier\Message\MessageOptionsInterface; /** * @author Alan Zarli * @author Farid Touil + * @author Alexandre Daubois */ final class SmsboxOptions implements MessageOptionsInterface { - public const MESSAGE_MODE_STANDARD = 'Standard'; - public const MESSAGE_MODE_EXPERT = 'Expert'; - public const MESSAGE_MODE_RESPONSE = 'Reponse'; - - public const MESSAGE_STRATEGY_PRIVATE = 1; - public const MESSAGE_STRATEGY_NOTIFICATION = 2; - public const MESSAGE_STRATEGY_NOT_MARKETING_GROUP = 3; - public const MESSAGE_STRATEGY_MARKETING = 4; - - public const MESSAGE_CODING_DEFAULT = 'default'; - public const MESSAGE_CODING_UNICODE = 'unicode'; - public const MESSAGE_CODING_AUTO = 'auto'; - - public const MESSAGE_CHARSET_ISO_1 = 'iso-8859-1'; - public const MESSAGE_CHARSET_ISO_15 = 'iso-8859-15'; - public const MESSAGE_CHARSET_UTF8 = 'utf-8'; - - public const MESSAGE_DAYS_MONDAY = 1; - public const MESSAGE_DAYS_TUESDAY = 2; - public const MESSAGE_DAYS_WEDNESDAY = 3; - public const MESSAGE_DAYS_THURSDAY = 4; - public const MESSAGE_DAYS_FRIDAY = 5; - public const MESSAGE_DAYS_SATURDAY = 6; - public const MESSAGE_DAYS_SUNDAY = 7; - - public const MESSAGE_UDH_6_OCTETS = 1; - public const MESSAGE_UDH_7_OCTETS = 2; - public const MESSAGE_UDH_DISABLED_CONCAT = 0; - - private array $options; - - public function __construct(array $options = []) + public function __construct(private array $options = []) { - $this->options = []; } public function getRecipientId(): null @@ -61,325 +37,230 @@ public function getRecipientId(): null return null; } - public function mode(string $mode) + /** + * @return $this + */ + public function mode(Mode $mode): static { - $this->options['mode'] = self::validateMode($mode); + $this->options['mode'] = $mode->value; return $this; } - public function strategy(int $strategy) + /** + * @return $this + */ + public function strategy(Strategy $strategy): static { - $this->options['strategy'] = self::validateStrategy($strategy); + $this->options['strategy'] = $strategy->value; return $this; } - public function date(string $date) + /** + * @return $this + */ + public function date(string $date): static { - $this->options['date'] = self::validateDate($date); + if (isset($this->options['dateTime'])) { + throw new InvalidArgumentException(sprintf('Either %1$s::dateTime() or %1$s::date() and %1$s::hour() must be called, but not both.', self::class)); + } - return $this; - } + if (!\DateTimeImmutable::createFromFormat('d/m/Y', $date)) { + throw new \DateMalformedStringException('The date must be in DD/MM/YYYY format.'); + } - public function hour(string $hour) - { - $this->options['heure'] = self::validateHour($hour); + $this->options['date'] = $date; return $this; } /** - * This method mustn't be set along with date and hour methods. + * @return $this */ - public function dateTime(\DateTime $dateTime) + public function hour(string $hour): static { - $this->options['dateTime'] = self::validateDateTime($dateTime); + if (isset($this->options['dateTime'])) { + throw new InvalidArgumentException(sprintf('Either %1$s::dateTime() or %1$s::date() and %1$s::hour() must be called, but not both.', self::class)); + } - return $this; - } + if (!\DateTimeImmutable::createFromFormat('H:i', $hour)) { + throw new \DateMalformedStringException('Hour must be in HH:MM format.'); + } - /** - * This method wait an ISO 3166-1 alpha. - */ - public function destIso(string $isoCode) - { - $this->options['dest_iso'] = self::validateDestIso($isoCode); + $this->options['heure'] = $hour; return $this; } /** - * This method will automatically set personnalise = 1 (according to SMSBOX documentation). + * @return $this */ - public function variable(array $variable) + public function dateTime(\DateTimeImmutable $dateTime): static { - $this->options['variable'] = $variable; + if (isset($this->options['date']) || isset($this->options['heure'])) { + throw new InvalidArgumentException(sprintf('Either %1$s::dateTime() or %1$s::date() and %1$s::hour() must be called, but not both.', self::class)); + } - return $this; - } + if ($dateTime < new \DateTimeImmutable('now')) { + throw new InvalidArgumentException('The given DateTime must be greater to the current date.'); + } - public function coding(string $coding) - { - $this->options['coding'] = self::validateCoding($coding); + $this->options['dateTime'] = $dateTime->setTimezone(new \DateTimeZone('Europe/Paris')); return $this; } - public function charset(string $charset) + /** + * An ISO 3166-1 alpha code. + * + * @return $this + */ + public function destIso(string $isoCode): static { - $this->options['charset'] = self::validateCharset($charset); - - return $this; - } + if (class_exists(Countries::class) && !Countries::exists($isoCode)) { + throw new InvalidArgumentException(sprintf('The country code "%s" is not valid.', $isoCode)); + } - public function udh(int $udh) - { - $this->options['udh'] = self::validateUdh($udh); + $this->options['dest_iso'] = $isoCode; return $this; } /** - * The true value = 1 in SMSBOX documentation. + * Automatically sets `personnalise` option to 1. + * + * @return $this */ - public function callback(bool $callback) + public function variable(array $variable): static { - $this->options['callback'] = $callback; + $this->options['variable'] = $variable; return $this; } /** - * The true value = 1 in SMSBOX documentation. + * @return $this */ - public function allowVocal(bool $allowVocal) + public function coding(Encoding $encoding): static { - $this->options['allow_vocal'] = $allowVocal; + $this->options['coding'] = $encoding->value; return $this; } - public function maxParts(int $maxParts) - { - $this->options['max_parts'] = self::validateMaxParts($maxParts); - - return $this; - } - - public function validity(int $validity) + /** + * @return $this + */ + public function charset(Charset $charset): static { - $this->options['validity'] = self::validateValidity($validity); + $this->options['charset'] = $charset->value; return $this; } - public function daysMinMax(int $min, int $max) + /** + * @return $this + */ + public function udh(Udh $udh): static { - $this->options['daysMinMax'] = self::validateDays($min, $max); + $this->options['udh'] = $udh->value; return $this; } - public function hoursMinMax(int $min, int $max) + /** + * @return $this + */ + public function callback(bool $callback): static { - $this->options['hoursMinMax'] = self::validateHours($min, $max); + $this->options['callback'] = $callback; return $this; } - public function sender(string $sender) + /** + * @return $this + */ + public function allowVocal(bool $allowVocal): static { - $this->options['sender'] = $sender; + $this->options['allow_vocal'] = $allowVocal; return $this; } - public function toArray(): array - { - return $this->options; - } - - public static function validateMode(string $mode): string - { - $supportedModes = [ - self::MESSAGE_MODE_STANDARD, - self::MESSAGE_MODE_EXPERT, - self::MESSAGE_MODE_RESPONSE, - ]; - - if (!\in_array($mode, $supportedModes, true)) { - throw new InvalidArgumentException(sprintf('The message mode "%s" is not supported; supported message modes are: "%s".', $mode, implode('", "', $supportedModes))); - } - - return $mode; - } - - public static function validateStrategy(int $strategy): int + /** + * @return $this + */ + public function maxParts(int $maxParts): static { - $supportedStrategies = [ - self::MESSAGE_STRATEGY_PRIVATE, - self::MESSAGE_STRATEGY_NOTIFICATION, - self::MESSAGE_STRATEGY_NOT_MARKETING_GROUP, - self::MESSAGE_STRATEGY_MARKETING, - ]; - if (!\in_array($strategy, $supportedStrategies, true)) { - throw new InvalidArgumentException(sprintf('The message strategy "%s" is not supported; supported strategies types are: "%s".', $strategy, implode('", "', $supportedStrategies))); + if ($maxParts < 1 || $maxParts > 8) { + throw new InvalidArgumentException(sprintf('The "max_parts" option must be an integer between 1 and 8, got "%d".', $maxParts)); } - return $strategy; - } - - public static function validateDate(string $date): string - { - $dateTimeObj = \DateTime::createFromFormat('d/m/Y', $date); - $now = new \DateTime(); - $tz = new \DateTimeZone('Europe/Paris'); - $dateTimeObj->setTimezone($tz); - $now->setTimezone($tz); - - if (!$dateTimeObj || $dateTimeObj->format('Y-m-d') <= (new \DateTime())->format('Y-m-d')) { - throw new InvalidArgumentException('The date must be in DD/MM/YYYY format and greater than the current date.'); - } + $this->options['max_parts'] = $maxParts; - return $date; - } - - public static function validateDateTime(\DateTime $dateTime) - { - \Locale::setDefault('fr'); - $now = new \DateTime(); - $tz = new \DateTimeZone('Europe/Paris'); - $now->setTimezone($tz); - $dateTime->setTimezone($tz); - - if ($now > $dateTime || $dateTime > $now->modify('+2 Year')) { - throw new InvalidArgumentException('dateTime must be greater to the actual date and limited to 2 years in the future.'); - } - - return $dateTime; + return $this; } - public static function validateDestIso(string $isoCode) + /** + * @return $this + */ + public function validity(int $validity): static { - if (!preg_match('/^[a-z]{2}$/i', $isoCode)) { - throw new InvalidArgumentException('destIso must be the ISO 3166-1 alpha 2 on two uppercase characters.'); + if ($validity < 5 || $validity > 1440) { + throw new InvalidArgumentException(sprintf('The "validity" option must be an integer between 5 and 1440, got "%d".', $validity)); } - return $isoCode; - } - - public static function validateHour(string $hour): string - { - $dateTimeObjhour = \DateTime::createFromFormat('H:i', $hour); - - if (!$dateTimeObjhour || $dateTimeObjhour->format('H:i') != $hour) { - throw new InvalidArgumentException('Hour must be in HH:MM format and valid.'); - } + $this->options['validity'] = $validity; - return $hour; + return $this; } - public static function validateCoding(string $coding): string + /** + * @return $this + */ + public function daysMinMax(Day $min, Day $max): static { - $supportedCodings = [ - self::MESSAGE_CODING_DEFAULT, - self::MESSAGE_CODING_UNICODE, - self::MESSAGE_CODING_AUTO, - ]; - - if (!\in_array($coding, $supportedCodings, true)) { - throw new InvalidArgumentException(sprintf('The message coding : "%s" is not supported; supported codings types are: "%s".', $coding, implode('", "', $supportedCodings))); + if (!$min->isBeforeOrEqualTo($max)) { + throw new InvalidArgumentException('The minimum day must be before the maximum day or the same.'); } - return $coding; - } + $this->options['daysMinMax'] = [$min->value, $max->value]; - public static function validateCharset(string $charset): string - { - $supportedCharsets = [ - self::MESSAGE_CHARSET_ISO_1, - self::MESSAGE_CHARSET_ISO_15, - self::MESSAGE_CHARSET_UTF8, - ]; - - if (!\in_array($charset, $supportedCharsets, true)) { - throw new InvalidArgumentException(sprintf('The message charset : "%s" is not supported; supported charsets types are: "%s".', $charset, implode('", "', $supportedCharsets))); - } - - return $charset; + return $this; } - public static function validateUdh(int $udh): int + /** + * @return $this + */ + public function hoursMinMax(int $min, int $max): static { - $supportedUdhs = [ - self::MESSAGE_UDH_6_OCTETS, - self::MESSAGE_UDH_7_OCTETS, - self::MESSAGE_UDH_DISABLED_CONCAT, - ]; - - if (!\in_array($udh, $supportedUdhs, true)) { - throw new InvalidArgumentException(sprintf('The message charset : "%s" is not supported; supported charsets types are: "%s".', $udh, implode('", "', $supportedUdhs))); + if ($min < 0 || $min > $max) { + throw new InvalidArgumentException('The minimum hour must be greater than 0 and lower than the maximum hour.'); } - return $udh; - } - - public static function validateMaxParts(int $maxParts): int - { - if ($maxParts < 1 || $maxParts > 8) { - throw new InvalidArgumentException(sprintf('The message max_parts : "%s" is not supported; supported max_parts values are integers between 1 and 8.', $maxParts)); + if ($max > 23) { + throw new InvalidArgumentException('The maximum hour must be lower or equal to 23.'); } - return $maxParts; - } + $this->options['hoursMinMax'] = [$min, $max]; - public static function validateValidity(int $validity): int - { - if ($validity < 5 || $validity > 1440) { - throw new InvalidArgumentException(sprintf('The message validity : "%s" is not supported; supported validity values are integers between 5 and 1440.', $validity)); - } - - return $validity; + return $this; } - public static function validateDays(int $min, int $max): array + /** + * @return $this + */ + public function sender(string $sender): static { - $supportedDays = [ - self::MESSAGE_DAYS_MONDAY, - self::MESSAGE_DAYS_TUESDAY, - self::MESSAGE_DAYS_WEDNESDAY, - self::MESSAGE_DAYS_THURSDAY, - self::MESSAGE_DAYS_FRIDAY, - self::MESSAGE_DAYS_SATURDAY, - self::MESSAGE_DAYS_SUNDAY, - ]; - - if (!\in_array($min, $supportedDays, true)) { - throw new InvalidArgumentException(sprintf('The message min : "%s" is not supported; supported charsets types are: "%s".', $min, implode('", "', $supportedDays))); - } - - if (!\in_array($max, $supportedDays, true)) { - throw new InvalidArgumentException(sprintf('The message max : "%s" is not supported; supported charsets types are: "%s".', $max, implode('", "', $supportedDays))); - } - - if ($min > $max) { - throw new InvalidArgumentException(sprintf('The message max must be greater than min.', $min)); - } + $this->options['sender'] = $sender; - return [$min, $max]; + return $this; } - public static function validateHours(int $min, int $max): array + public function toArray(): array { - if ($min < 0 || $min > $max) { - throw new InvalidArgumentException(sprintf('The message min : "%s" is not supported; supported min values are integers between 0 and 23.', $min)); - } - - if ($max > 23) { - throw new InvalidArgumentException(sprintf('The message max : "%s" is not supported; supported min values are integers between 0 and 23.', $max)); - } - - return [$min, $max]; + return $this->options; } } diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransport.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransport.php index 899acaddd328c..b951dc7673545 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransport.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Notifier\Bridge\Smsbox; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Mode; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Strategy; use Symfony\Component\Notifier\Exception\InvalidArgumentException; use Symfony\Component\Notifier\Exception\TransportException; use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; @@ -30,31 +32,26 @@ final class SmsboxTransport extends AbstractTransport { protected const HOST = 'api.smsbox.pro'; - private string $apiKey; - private ?string $mode; - private ?int $strategy; - private ?string $sender; - - public function __construct(#[\SensitiveParameter] string $apiKey, string $mode, int $strategy, ?string $sender, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) - { - $this->apiKey = $apiKey; - $this->mode = $mode; - $this->strategy = $strategy; - $this->sender = $sender; - - SmsboxOptions::validateMode($this->mode); - SmsboxOptions::validateStrategy($this->strategy); - + public function __construct( + #[\SensitiveParameter] private string $apiKey, + private Mode $mode, + private Strategy $strategy, + private ?string $sender, + HttpClientInterface $client = null, + EventDispatcherInterface $dispatcher = null + ) { parent::__construct($client, $dispatcher); } public function __toString(): string { - if (SmsboxOptions::MESSAGE_MODE_EXPERT === $this->mode) { - return sprintf('smsbox://%s?mode=%s&strategy=%s&sender=%s', $this->getEndpoint(), $this->mode, $this->strategy, $this->sender); + $dsn = sprintf('smsbox://%s?mode=%s&strategy=%s', $this->getEndpoint(), $this->mode->value, $this->strategy->value); + + if (Mode::Expert === $this->mode) { + $dsn .= '&sender='.$this->sender; } - return sprintf('smsbox://%s?mode=%s&strategy=%s', $this->getEndpoint(), $this->mode, $this->strategy); + return $dsn; } public function supports(MessageInterface $message): bool @@ -78,49 +75,45 @@ protected function doSend(MessageInterface $message): SentMessage $options['msg'] = $message->getSubject(); $options['id'] = 1; $options['usage'] = 'symfony'; - $options['mode'] ??= $this->mode; - $options['strategy'] ??= $this->strategy; + $options['mode'] ??= $this->mode->value; + $options['strategy'] ??= $this->strategy->value; - if (SmsboxOptions::MESSAGE_MODE_EXPERT === $options['mode']) { + if (Mode::Expert === $options['mode']) { $options['origine'] = $options['sender'] ?? $this->sender; } unset($options['sender']); if (isset($options['daysMinMax'])) { - $options['day_min'] = $options['daysMinMax'][0]; - $options['day_max'] = $options['daysMinMax'][1]; + [$options['day_min'], $options['day_max']] = $options['daysMinMax']; unset($options['daysMinMax']); } if (isset($options['hoursMinMax'])) { - $options['hour_min'] = $options['hoursMinMax'][0]; - $options['hour_max'] = $options['hoursMinMax'][1]; + [$options['hour_min'], $options['hour_max']] = $options['hoursMinMax']; unset($options['hoursMinMax']); } if (isset($options['dateTime'])) { - if (isset($options['heure']) || isset($options['date'])) { - throw new InvalidArgumentException("You mustn't set the dateTime method along with date or hour methods."); - } - $options['date'] = $options['dateTime']->format('d/m/Y'); $options['heure'] = $options['dateTime']->format('H:i'); + unset($options['dateTime']); } if (isset($options['variable'])) { preg_match_all('%([0-9]+)%', $options['msg'], $matches); - $occurenceValMsg = $matches[0]; - $occurenceValMsgMax = max($occurenceValMsg); + $occurrenceValMsg = $matches[0]; + $occurrenceValMsgMax = (int) max($occurrenceValMsg); - if ($occurenceValMsgMax != \count($options['variable'])) { - throw new InvalidArgumentException('You must have the same amount of index in your array as you have variable.'); + if ($occurrenceValMsgMax !== \count($options['variable'])) { + throw new InvalidArgumentException(sprintf('You must have the same amount of index in your array as you have variable. Expected %d variable, got %d.', $occurrenceValMsgMax, \count($options['variable']))); } $t = str_replace([',', ';'], ['%d44%', '%d59%'], $options['variable']); $variableStr = implode(';', $t); $options['dest'] .= ';'.$variableStr; $options['personnalise'] = 1; + unset($options['variable']); } @@ -135,11 +128,12 @@ protected function doSend(MessageInterface $message): SentMessage try { $statusCode = $response->getStatusCode(); } catch (TransportExceptionInterface $e) { - throw new TransportException('Could not reach the remote Smsbox server.', $response, 0, $e); + throw new TransportException('Could not reach the remote Smsbox server.', $response, previous: $e); } if (200 !== $statusCode) { - $error = $response->getContent(false); + $error = $response->toArray(false); + throw new TransportException(sprintf('Unable to send the SMS: "%s" (%s).', $error['description'], $error['code']), $response); } diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransportFactory.php index 9a1f386581a86..fc14bc07d5d37 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransportFactory.php +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/SmsboxTransportFactory.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Notifier\Bridge\Smsbox; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Mode; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Strategy; use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; use Symfony\Component\Notifier\Transport\AbstractTransportFactory; use Symfony\Component\Notifier\Transport\Dsn; @@ -30,18 +32,20 @@ public function create(Dsn $dsn): SmsboxTransport } $apiKey = $this->getUser($dsn); - $mode = $dsn->getRequiredOption('mode'); - $strategy = $dsn->getRequiredOption('strategy'); + $mode = Mode::from($dsn->getRequiredOption('mode')); + $strategy = Strategy::from($dsn->getRequiredOption('strategy')); $sender = $dsn->getOption('sender'); - if (SmsboxOptions::MESSAGE_MODE_EXPERT === $mode) { + if (Mode::Expert === $mode) { $sender = $dsn->getRequiredOption('sender'); } $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); $port = $dsn->getPort(); - return (new SmsboxTransport($apiKey, $mode, $strategy, $sender, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + return (new SmsboxTransport($apiKey, $mode, $strategy, $sender, $this->client, $this->dispatcher)) + ->setHost($host) + ->setPort($port); } protected function getSupportedSchemes(): array diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxOptionsTest.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxOptionsTest.php index 353dba391efe5..1f158d8eb4f50 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxOptionsTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxOptionsTest.php @@ -12,6 +12,12 @@ namespace Symfony\Component\Notifier\Bridge\Smsbox\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Intl\Countries; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Charset; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Day; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Mode; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Strategy; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Udh; use Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions; use Symfony\Component\Notifier\Exception\InvalidArgumentException; @@ -20,11 +26,11 @@ class SmsboxOptionsTest extends TestCase public function testSmsboxOptions() { $smsboxOptions = (new SmsboxOptions()) - ->mode(SmsboxOptions::MESSAGE_MODE_EXPERT) + ->mode(Mode::Expert) ->sender('SENDER') - ->strategy(SmsboxOptions::MESSAGE_STRATEGY_MARKETING) - ->charset(SmsboxOptions::MESSAGE_CHARSET_UTF8) - ->udh(SmsboxOptions::MESSAGE_UDH_DISABLED_CONCAT) + ->strategy(Strategy::Marketing) + ->charset(Charset::Utf8) + ->udh(Udh::DisabledConcat) ->maxParts(2) ->validity(100) ->destIso('FR'); @@ -41,37 +47,148 @@ public function testSmsboxOptions() ], $smsboxOptions->toArray()); } - public function testSmsboxOptionsInvalidMode() + public function testSmsboxOptionsInvalidDestIso() { + if (!class_exists(Countries::class)) { + $this->markTestSkipped('The "symfony/intl" component is required to run this test.'); + } + $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The message mode "XXXXXX" is not supported; supported message modes are: "Standard", "Expert", "Reponse"'); + $this->expectExceptionMessage('The country code "X1" is not valid.'); - $smsboxOptions = (new SmsboxOptions()) - ->mode('XXXXXX') + (new SmsboxOptions()) + ->mode(Mode::Expert) ->sender('SENDER') - ->strategy(SmsboxOptions::MESSAGE_STRATEGY_MARKETING); + ->strategy(Strategy::Marketing) + ->destIso('X1'); } - public function testSmsboxOptionsInvalidStrategy() + public function testDateIsCalledWithDateTime() { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The message strategy "10" is not supported; supported strategies types are: "1", "2", "3", "4"'); + $this->expectExceptionMessage('Either Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::dateTime() or Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::date() and Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::hour() must be called, but not both.'); - $smsboxOptions = (new SmsboxOptions()) - ->mode(SmsboxOptions::MESSAGE_MODE_STANDARD) - ->sender('SENDER') - ->strategy(10); + (new SmsboxOptions()) + ->dateTime(new \DateTimeImmutable('+1 day')) + ->date('01/01/2021'); } - public function testSmsboxOptionsInvalidDestIso() + public function testDateInWrongFormat() + { + $this->expectException(\DateMalformedStringException::class); + $this->expectExceptionMessage('The date must be in DD/MM/YYYY format.'); + + (new SmsboxOptions()) + ->date('01/2021'); + } + + public function testHourIsCalledWithDateTime() { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('destIso must be the ISO 3166-1 alpha 2 on two uppercase characters.'); + $this->expectExceptionMessage('Either Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::dateTime() or Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::date() and Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::hour() must be called, but not both.'); - $smsboxOptions = (new SmsboxOptions()) - ->mode(SmsboxOptions::MESSAGE_MODE_EXPERT) - ->sender('SENDER') - ->strategy(SmsboxOptions::MESSAGE_STRATEGY_MARKETING) - ->destIso('X1'); + (new SmsboxOptions()) + ->dateTime(new \DateTimeImmutable('+1 day')) + ->hour('12:00'); + } + + public function testHourInWrongFormat() + { + $this->expectException(\DateMalformedStringException::class); + $this->expectExceptionMessage('Hour must be in HH:MM format.'); + + (new SmsboxOptions()) + ->hour('12:00:00'); + } + + public function testDateTimeIsCalledWithDate() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Either Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::dateTime() or Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::date() and Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::hour() must be called, but not both.'); + + (new SmsboxOptions()) + ->date('01/01/2021') + ->dateTime(new \DateTimeImmutable('+1 day')); + } + + public function testDateTimeIsCalledWithHour() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Either Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::dateTime() or Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::date() and Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::hour() must be called, but not both.'); + + (new SmsboxOptions()) + ->hour('12:00') + ->dateTime(new \DateTimeImmutable('+1 day')); + } + + public function testDateTimeIsInPast() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The given DateTime must be greater to the current date.'); + + (new SmsboxOptions()) + ->dateTime(new \DateTimeImmutable('-1 day')); + } + + /** + * @testWith [0] + * [9] + */ + public function testMaxPartIsInvalid(int $maxPart) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('The "max_parts" option must be an integer between 1 and 8, got "%d".', $maxPart)); + + (new SmsboxOptions()) + ->maxParts($maxPart); + } + + /** + * @testWith [4] + * [1441] + */ + public function testValidityIsInvalid(int $validity) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('The "validity" option must be an integer between 5 and 1440, got "%d".', $validity)); + + (new SmsboxOptions()) + ->validity($validity); + } + + public function testDayMinIsAfterMax() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The minimum day must be before the maximum day or the same.'); + + (new SmsboxOptions()) + ->daysMinMax(Day::Sunday, Day::Friday); + } + + public function testHourIsNegative() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The minimum hour must be greater than 0 and lower than the maximum hour.'); + + (new SmsboxOptions()) + ->hoursMinMax(-1, 12); + } + + public function testMinHourIsAfterMax() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The minimum hour must be greater than 0 and lower than the maximum hour.'); + + (new SmsboxOptions()) + ->hoursMinMax(12, 11); + } + + public function testMaxHourIsOutOfBounds() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The maximum hour must be lower or equal to 23.'); + + (new SmsboxOptions()) + ->hoursMinMax(0, 24); } } diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportTest.php index 0ab5c763d72ed..a85ca6a8ed647 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/Tests/SmsboxTransportTest.php @@ -12,6 +12,10 @@ namespace Symfony\Component\Notifier\Bridge\Smsbox\Tests; use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Day; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Encoding; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Mode; +use Symfony\Component\Notifier\Bridge\Smsbox\Enum\Strategy; use Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions; use Symfony\Component\Notifier\Bridge\Smsbox\SmsboxTransport; use Symfony\Component\Notifier\Exception\TransportException; @@ -26,7 +30,7 @@ final class SmsboxTransportTest extends TransportTestCase { public static function createTransport(HttpClientInterface $client = null): SmsboxTransport { - return new SmsboxTransport('apikey', 'Standard', 4, null, $client ?? new MockHttpClient()); + return new SmsboxTransport('apikey', Mode::Standard, Strategy::Marketing, null, $client ?? new MockHttpClient()); } public static function toStringProvider(): iterable @@ -49,18 +53,18 @@ public function testBasicQuerySucceded() { $message = new SmsMessage('+33612345678', 'Hello!'); $response = $this->createMock(ResponseInterface::class); - $response->expects(self::exactly(2)) + $response->expects($this->exactly(2)) ->method('getStatusCode') ->willReturn(200); - $response->expects(self::once()) + $response->expects($this->once()) ->method('getContent') ->willReturn('OK 12345678'); $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { - self::assertSame('POST', $method); - self::assertSame('https://api.smsbox.pro/1.1/api.php', $url); - self::assertSame('dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4', $request['body']); + $this->assertSame('POST', $method); + $this->assertSame('https://api.smsbox.pro/1.1/api.php', $url); + $this->assertSame('dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4', $request['body']); return $response; }); @@ -68,7 +72,7 @@ public function testBasicQuerySucceded() $transport = $this->createTransport($client); $sentMessage = $transport->send($message); - self::assertSame('12345678', $sentMessage->getMessageId()); + $this->assertSame('12345678', $sentMessage->getMessageId()); } public function testBasicQueryFailed() @@ -78,18 +82,18 @@ public function testBasicQueryFailed() $message = new SmsMessage('+33612345678', 'Hello!'); $response = $this->createMock(ResponseInterface::class); - $response->expects(self::exactly(2)) + $response->expects($this->exactly(2)) ->method('getStatusCode') ->willReturn(200); - $response->expects(self::once()) + $response->expects($this->once()) ->method('getContent') ->willReturn('ERROR 02'); $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { - self::assertSame('POST', $method); - self::assertSame('https://api.smsbox.pro/1.1/api.php', $url); - self::assertSame('dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4', $request['body']); + $this->assertSame('POST', $method); + $this->assertSame('https://api.smsbox.pro/1.1/api.php', $url); + $this->assertSame('dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4', $request['body']); return $response; }); @@ -102,18 +106,18 @@ public function testQuerySuccededWithOptions() { $message = new SmsMessage('+33612345678', 'Hello!'); $response = $this->createMock(ResponseInterface::class); - $response->expects(self::exactly(2)) + $response->expects($this->exactly(2)) ->method('getStatusCode') ->willReturn(200); - $response->expects(self::once()) + $response->expects($this->once()) ->method('getContent') ->willReturn('OK 12345678'); $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { - self::assertSame('POST', $method); - self::assertSame('https://api.smsbox.pro/1.1/api.php', $url); - self::assertSame('max_parts=5&coding=unicode&callback=1&dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4&day_min=1&day_max=3', $request['body']); + $this->assertSame('POST', $method); + $this->assertSame('https://api.smsbox.pro/1.1/api.php', $url); + $this->assertSame('max_parts=5&coding=unicode&callback=1&dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4&day_min=1&day_max=3', $request['body']); return $response; }); @@ -121,37 +125,37 @@ public function testQuerySuccededWithOptions() $transport = $this->createTransport($client); $options = (new SmsboxOptions()) ->maxParts(5) - ->coding(SmsboxOptions::MESSAGE_CODING_UNICODE) - ->daysMinMax(1, 3) + ->coding(Encoding::Unicode) + ->daysMinMax(Day::Monday, Day::Wednesday) ->callback(true); $message->options($options); $sentMessage = $transport->send($message); - self::assertSame('12345678', $sentMessage->getMessageId()); + $this->assertSame('12345678', $sentMessage->getMessageId()); } public function testQueryDateTime() { $message = new SmsMessage('+33612345678', 'Hello!'); $response = $this->createMock(ResponseInterface::class); - $response->expects(self::exactly(2)) + $response->expects($this->exactly(2)) ->method('getStatusCode') ->willReturn(200); - $response->expects(self::once()) + $response->expects($this->once()) ->method('getContent') ->willReturn('OK 12345678'); $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { - self::assertSame('POST', $method); - self::assertSame('https://api.smsbox.pro/1.1/api.php', $url); - self::assertSame('dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4&date=05%2F12%2F2025&heure=19%3A00', $request['body']); + $this->assertSame('POST', $method); + $this->assertSame('https://api.smsbox.pro/1.1/api.php', $url); + $this->assertSame('dest=%2B33612345678&msg=Hello%21&id=1&usage=symfony&mode=Standard&strategy=4&date=05%2F12%2F2025&heure=19%3A00', $request['body']); return $response; }); - $dateTime = \DateTime::createFromFormat('d/m/Y H:i', '05/12/2025 18:00', new \DateTimeZone('UTC')); + $dateTime = \DateTimeImmutable::createFromFormat('d/m/Y H:i', '05/12/2025 18:00', new \DateTimeZone('UTC')); $transport = $this->createTransport($client); @@ -161,25 +165,25 @@ public function testQueryDateTime() $message->options($options); $sentMessage = $transport->send($message); - self::assertSame('12345678', $sentMessage->getMessageId()); + $this->assertSame('12345678', $sentMessage->getMessageId()); } public function testQueryVariable() { $message = new SmsMessage('0612345678', 'Hello %1% %2%'); $response = $this->createMock(ResponseInterface::class); - $response->expects(self::exactly(2)) + $response->expects($this->exactly(2)) ->method('getStatusCode') ->willReturn(200); - $response->expects(self::once()) + $response->expects($this->once()) ->method('getContent') ->willReturn('OK 12345678'); $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { - self::assertSame('POST', $method); - self::assertSame('https://api.smsbox.pro/1.1/api.php', $url); - self::assertSame('dest=0612345678%3Btye%25d44%25%25d44%25t%3Be%25d59%25%25d44%25fe&msg=Hello+%251%25+%252%25&id=1&usage=symfony&mode=Standard&strategy=4&personnalise=1', $request['body']); + $this->assertSame('POST', $method); + $this->assertSame('https://api.smsbox.pro/1.1/api.php', $url); + $this->assertSame('dest=0612345678%3Btye%25d44%25%25d44%25t%3Be%25d59%25%25d44%25fe&msg=Hello+%251%25+%252%25&id=1&usage=symfony&mode=Standard&strategy=4&personnalise=1', $request['body']); return $response; }); @@ -192,25 +196,25 @@ public function testQueryVariable() $message->options($options); $sentMessage = $transport->send($message); - self::assertSame('12345678', $sentMessage->getMessageId()); + $this->assertSame('12345678', $sentMessage->getMessageId()); } public function testSmsboxOptionsInvalidDateTimeAndDate() { $response = $this->createMock(ResponseInterface::class); - $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { + $client = new MockHttpClient(function () use ($response): ResponseInterface { return $response; }); $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage("You mustn't set the dateTime method along with date or hour methods"); - $dateTime = \DateTime::createFromFormat('d/m/Y H:i', '01/11/2024 18:00', new \DateTimeZone('UTC')); + $this->expectExceptionMessage("Either Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::dateTime() or Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::date() and Symfony\Component\Notifier\Bridge\Smsbox\SmsboxOptions::hour() must be called, but not both."); + $dateTime = \DateTimeImmutable::createFromFormat('d/m/Y H:i', '01/11/2024 18:00', new \DateTimeZone('UTC')); $message = new SmsMessage('+33612345678', 'Hello'); $smsboxOptions = (new SmsboxOptions()) - ->mode(SmsboxOptions::MESSAGE_MODE_EXPERT) + ->mode(Mode::Expert) ->sender('SENDER') - ->strategy(SmsboxOptions::MESSAGE_STRATEGY_MARKETING) + ->strategy(Strategy::Marketing) ->dateTime($dateTime) ->date('01/01/2024'); @@ -223,7 +227,7 @@ public function testSmsboxOptionsInvalidDateTimeAndDate() public function testSmsboxInvalidPhoneNumber() { $response = $this->createMock(ResponseInterface::class); - $client = new MockHttpClient(function (string $method, string $url, $request) use ($response): ResponseInterface { + $client = new MockHttpClient(function () use ($response): ResponseInterface { return $response; }); @@ -232,9 +236,9 @@ public function testSmsboxInvalidPhoneNumber() $message = new SmsMessage('+336123456789000000', 'Hello'); $smsboxOptions = (new SmsboxOptions()) - ->mode(SmsboxOptions::MESSAGE_MODE_EXPERT) + ->mode(Mode::Expert) ->sender('SENDER') - ->strategy(SmsboxOptions::MESSAGE_STRATEGY_MARKETING); + ->strategy(Strategy::Marketing); $transport = $this->createTransport($client); $message->options($smsboxOptions); diff --git a/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json b/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json index 63dfca4eb99d5..35d74d2a9dd3b 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Smsbox/composer.json @@ -26,7 +26,8 @@ "require": { "php": ">=8.2", "symfony/http-client": "^6.4|^7.0", - "symfony/notifier": "^7.1" + "symfony/notifier": "^7.1", + "symfony/polyfill-php83": "^1.28" }, "autoload": { "psr-4": { From d4a5524dd986f4adc5184212c6305c23b72c41cd Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 26 Dec 2023 17:19:00 +0100 Subject: [PATCH 073/395] rename addHeader() to setHeader() --- src/Symfony/Component/HttpClient/CHANGELOG.md | 1 + src/Symfony/Component/HttpClient/HttpOptions.php | 3 +-- .../Component/HttpClient/Tests/HttpOptionsTest.php | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md index 4e9e09ee263e3..581247bbab847 100644 --- a/src/Symfony/Component/HttpClient/CHANGELOG.md +++ b/src/Symfony/Component/HttpClient/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.1 --- + * Add `HttpOptions::setHeader()` to add or replace a single header * Allow mocking `start_time` info in `MockResponse` * Add `MockResponse::fromFile()` and `JsonMockResponse::fromFile()` methods to help using fixtures files diff --git a/src/Symfony/Component/HttpClient/HttpOptions.php b/src/Symfony/Component/HttpClient/HttpOptions.php index 8eba8ba055f7c..b256bb3ba1596 100644 --- a/src/Symfony/Component/HttpClient/HttpOptions.php +++ b/src/Symfony/Component/HttpClient/HttpOptions.php @@ -66,9 +66,8 @@ public function setQuery(array $query): static /** * @return $this */ - public function addHeader(string $key, string $value): static + public function setHeader(string $key, string $value): static { - $this->options['headers'] ??= []; $this->options['headers'][$key] = $value; return $this; diff --git a/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php b/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php index 487a889d454f7..906dfc5bbf17a 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php @@ -40,14 +40,14 @@ public function testSetAuthBearer() $this->assertSame('foobar', (new HttpOptions())->setAuthBearer('foobar')->toArray()['auth_bearer']); } - public function testAddHeader() + public function testSetHeader() { $options = new HttpOptions(); - $options->addHeader('Accept', 'application/json'); + $options->setHeader('Accept', 'application/json'); $this->assertSame(['Accept' => 'application/json'], $options->toArray()['headers']); - $options->addHeader('Accept-Language', 'en-US,en;q=0.5'); + $options->setHeader('Accept-Language', 'en-US,en;q=0.5'); $this->assertSame(['Accept' => 'application/json', 'Accept-Language' => 'en-US,en;q=0.5'], $options->toArray()['headers']); - $options->addHeader('Accept', 'application/html'); + $options->setHeader('Accept', 'application/html'); $this->assertSame(['Accept' => 'application/html', 'Accept-Language' => 'en-US,en;q=0.5'], $options->toArray()['headers']); } } From 98f4fa1bd9f96772fef2740d69a5d11293e3819c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 26 Dec 2023 11:22:59 +0100 Subject: [PATCH 074/395] [HttpKernel] Add `HttpException::fromStatusCode()` --- src/Symfony/Component/HttpKernel/CHANGELOG.md | 1 + .../RequestPayloadValueResolver.php | 19 +++++------ .../EventListener/ErrorListener.php | 4 +-- .../HttpKernel/Exception/HttpException.php | 21 ++++++++++++ .../Tests/Exception/HttpExceptionTest.php | 32 +++++++++++++++++++ 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index eb0e3c1cb44e0..45761dfbbed42 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add method `isKernelTerminating()` to `ExceptionEvent` that allows to check if an exception was thrown while the kernel is being terminated + * Add `HttpException::fromStatusCode()` 7.0 --- diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php index 444be1b3fe7d2..85e94c235de12 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -13,13 +13,14 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Serializer\Exception\NotEncodableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; @@ -124,13 +125,13 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo } if (\count($violations)) { - throw new HttpException($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations)); + throw HttpException::fromStatusCode($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations)); } } else { try { $payload = $this->$payloadMapper($request, $type, $argument); } catch (PartialDenormalizationException $e) { - throw new HttpException($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), $e->getErrors())), $e); + throw HttpException::fromStatusCode($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), $e->getErrors())), $e); } } @@ -138,7 +139,7 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo $payload = match (true) { $argument->metadata->hasDefaultValue() => $argument->metadata->getDefaultValue(), $argument->metadata->isNullable() => null, - default => throw new HttpException($validationFailedCode) + default => throw HttpException::fromStatusCode($validationFailedCode) }; } @@ -167,11 +168,11 @@ private function mapQueryString(Request $request, string $type, MapQueryString $ private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): ?object { if (null === $format = $request->getContentTypeFormat()) { - throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, 'Unsupported format.'); + throw new UnsupportedMediaTypeHttpException('Unsupported format.'); } if ($attribute->acceptFormat && !\in_array($format, (array) $attribute->acceptFormat, true)) { - throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format)); + throw new UnsupportedMediaTypeHttpException(sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format)); } if ($data = $request->request->all()) { @@ -183,15 +184,15 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay } if ('form' === $format) { - throw new HttpException(Response::HTTP_BAD_REQUEST, 'Request payload contains invalid "form" data.'); + throw new BadRequestHttpException('Request payload contains invalid "form" data.'); } try { return $this->serializer->deserialize($data, $type, $format, self::CONTEXT_DESERIALIZE + $attribute->serializationContext); } catch (UnsupportedFormatException $e) { - throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, sprintf('Unsupported format: "%s".', $format), $e); + throw new UnsupportedMediaTypeHttpException(sprintf('Unsupported format: "%s".', $format), $e); } catch (NotEncodableValueException $e) { - throw new HttpException(Response::HTTP_BAD_REQUEST, sprintf('Request payload contains invalid "%s" data.', $format), $e); + throw new BadRequestHttpException(sprintf('Request payload contains invalid "%s" data.', $format), $e); } } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php index cf52851928f86..6e47baf2ee6c3 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php @@ -63,7 +63,7 @@ public function logKernelException(ExceptionEvent $event): void } if (!$throwable instanceof HttpExceptionInterface || $throwable->getStatusCode() !== $config['status_code']) { $headers = $throwable instanceof HttpExceptionInterface ? $throwable->getHeaders() : []; - $throwable = new HttpException($config['status_code'], $throwable->getMessage(), $throwable, $headers); + $throwable = HttpException::fromStatusCode($config['status_code'], $throwable->getMessage(), $throwable, $headers); $event->setThrowable($throwable); } break; @@ -78,7 +78,7 @@ public function logKernelException(ExceptionEvent $event): void /** @var WithHttpStatus $instance */ $instance = $attributes[0]->newInstance(); - $throwable = new HttpException($instance->statusCode, $throwable->getMessage(), $throwable, $instance->headers); + $throwable = HttpException::fromStatusCode($instance->statusCode, $throwable->getMessage(), $throwable, $instance->headers); $event->setThrowable($throwable); break; } diff --git a/src/Symfony/Component/HttpKernel/Exception/HttpException.php b/src/Symfony/Component/HttpKernel/Exception/HttpException.php index f95f77bcafae9..7eaf049e9302c 100644 --- a/src/Symfony/Component/HttpKernel/Exception/HttpException.php +++ b/src/Symfony/Component/HttpKernel/Exception/HttpException.php @@ -29,6 +29,27 @@ public function __construct(int $statusCode, string $message = '', \Throwable $p parent::__construct($message, $code, $previous); } + public static function fromStatusCode(int $statusCode, string $message = '', \Throwable $previous = null, array $headers = [], int $code = 0): self + { + return match ($statusCode) { + 400 => new BadRequestHttpException($message, $previous, $code, $headers), + 403 => new AccessDeniedHttpException($message, $previous, $code, $headers), + 404 => new NotFoundHttpException($message, $previous, $code, $headers), + 406 => new NotAcceptableHttpException($message, $previous, $code, $headers), + 409 => new ConflictHttpException($message, $previous, $code, $headers), + 410 => new GoneHttpException($message, $previous, $code, $headers), + 411 => new LengthRequiredHttpException($message, $previous, $code, $headers), + 412 => new PreconditionFailedHttpException($message, $previous, $code, $headers), + 423 => new LockedHttpException($message, $previous, $code, $headers), + 415 => new UnsupportedMediaTypeHttpException($message, $previous, $code, $headers), + 422 => new UnprocessableEntityHttpException($message, $previous, $code, $headers), + 428 => new PreconditionRequiredHttpException($message, $previous, $code, $headers), + 429 => new TooManyRequestsHttpException(null, $message, $previous, $code, $headers), + 503 => new ServiceUnavailableHttpException(null, $message, $previous, $code, $headers), + default => new static($statusCode, $message, $previous, $headers, $code), + }; + } + public function getStatusCode(): int { return $this->statusCode; diff --git a/src/Symfony/Component/HttpKernel/Tests/Exception/HttpExceptionTest.php b/src/Symfony/Component/HttpKernel/Tests/Exception/HttpExceptionTest.php index fad9e796f439b..11636bbb60bd5 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Exception/HttpExceptionTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Exception/HttpExceptionTest.php @@ -63,6 +63,38 @@ public function testThrowableIsAllowedForPrevious() $this->assertSame($previous, $exception->getPrevious()); } + /** + * @dataProvider provideStatusCode + */ + public function testFromStatusCode(int $statusCode) + { + $exception = HttpException::fromStatusCode($statusCode); + $this->assertInstanceOf(HttpException::class, $exception); + $this->assertSame($statusCode, $exception->getStatusCode()); + } + + public static function provideStatusCode() + { + return [ + [400], + [401], + [403], + [404], + [406], + [409], + [410], + [411], + [412], + [418], + [423], + [415], + [422], + [428], + [429], + [503], + ]; + } + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new HttpException(200, $message, $previous, $headers, $code); From 19de235d2a286a46c685c95760c9c942e3bd08a5 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo Date: Sat, 23 Dec 2023 00:08:01 +0700 Subject: [PATCH 075/395] [HttpKernel] Allow `#[WithHttpStatus]` and `#[WithLogLevel]` to take effect on interfaces --- .../EventListener/ErrorListener.php | 70 +++++++++++++------ .../Tests/EventListener/ErrorListenerTest.php | 39 +++++++++++ 2 files changed, 86 insertions(+), 23 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php index 6e47baf2ee6c3..3934d9abe4280 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php @@ -70,19 +70,9 @@ public function logKernelException(ExceptionEvent $event): void } // There's no specific status code defined in the configuration for this exception - if (!$throwable instanceof HttpExceptionInterface) { - $class = new \ReflectionClass($throwable); - - do { - if ($attributes = $class->getAttributes(WithHttpStatus::class, \ReflectionAttribute::IS_INSTANCEOF)) { - /** @var WithHttpStatus $instance */ - $instance = $attributes[0]->newInstance(); - - $throwable = HttpException::fromStatusCode($instance->statusCode, $throwable->getMessage(), $throwable, $instance->headers); - $event->setThrowable($throwable); - break; - } - } while ($class = $class->getParentClass()); + if (!$throwable instanceof HttpExceptionInterface && $withHttpStatus = $this->getInheritedAttribute($throwable::class, WithHttpStatus::class)) { + $throwable = HttpException::fromStatusCode($withHttpStatus->statusCode, $throwable->getMessage(), $throwable, $withHttpStatus->headers); + $event->setThrowable($throwable); } $e = FlattenException::createFromThrowable($throwable); @@ -200,16 +190,9 @@ private function resolveLogLevel(\Throwable $throwable): string } } - $class = new \ReflectionClass($throwable); - - do { - if ($attributes = $class->getAttributes(WithLogLevel::class)) { - /** @var WithLogLevel $instance */ - $instance = $attributes[0]->newInstance(); - - return $instance->level; - } - } while ($class = $class->getParentClass()); + if ($withLogLevel = $this->getInheritedAttribute($throwable::class, WithLogLevel::class)) { + return $withLogLevel->level; + } if (!$throwable instanceof HttpExceptionInterface || $throwable->getStatusCode() >= 500) { return LogLevel::CRITICAL; @@ -233,4 +216,45 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re return $request; } + + /** + * @template T + * + * @param class-string $attribute + * + * @return T|null + */ + private function getInheritedAttribute(string $class, string $attribute): ?object + { + $class = new \ReflectionClass($class); + $interfaces = []; + $attributeReflector = null; + $parentInterfaces = []; + $ownInterfaces = []; + + do { + if ($attributes = $class->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { + $attributeReflector = $attributes[0]; + $parentInterfaces = class_implements($class->name); + break; + } + + $interfaces[] = class_implements($class->name); + } while ($class = $class->getParentClass()); + + while ($interfaces) { + $ownInterfaces = array_diff_key(array_pop($interfaces), $parentInterfaces); + $parentInterfaces += $ownInterfaces; + + foreach ($ownInterfaces as $interface) { + $class = new \ReflectionClass($interface); + + if ($attributes = $class->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { + $attributeReflector = $attributes[0]; + } + } + } + + return $attributeReflector?->newInstance(); + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php index 214178984c2ff..7b9e7ce0667e8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php @@ -139,6 +139,21 @@ public function testHandleWithLogLevelAttribute() $this->assertCount(1, $logger->getLogs('warning')); } + public function testHandleClassImplementingInterfaceWithLogLevelAttribute() + { + $request = new Request(); + $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new ImplementingInterfaceWithLogLevelAttribute()); + $logger = new TestLogger(); + $l = new ErrorListener('not used', $logger); + + $l->logKernelException($event); + $l->onKernelException($event); + + $this->assertEquals(0, $logger->countErrors()); + $this->assertCount(0, $logger->getLogs('critical')); + $this->assertCount(1, $logger->getLogs('warning')); + } + public function testHandleWithLogLevelAttributeAndCustomConfiguration() { $request = new Request(); @@ -298,6 +313,7 @@ public static function exceptionWithAttributeProvider() yield [new WithCustomUserProvidedAttribute(), 208, ['name' => 'value']]; yield [new WithGeneralAttribute(), 412, ['some' => 'thing']]; yield [new ChildOfWithGeneralAttribute(), 412, ['some' => 'thing']]; + yield [new ImplementingInterfaceWithGeneralAttribute(), 412, ['some' => 'thing']]; } } @@ -359,6 +375,20 @@ class WithGeneralAttribute extends \Exception { } +#[WithHttpStatus( + statusCode: Response::HTTP_PRECONDITION_FAILED, + headers: [ + 'some' => 'thing', + ] +)] +interface InterfaceWithGeneralAttribute +{ +} + +class ImplementingInterfaceWithGeneralAttribute extends \Exception implements InterfaceWithGeneralAttribute +{ +} + class ChildOfWithGeneralAttribute extends WithGeneralAttribute { } @@ -371,3 +401,12 @@ class WarningWithLogLevelAttribute extends \Exception class ChildOfWarningWithLogLevelAttribute extends WarningWithLogLevelAttribute { } + +#[WithLogLevel(LogLevel::WARNING)] +interface InterfaceWithLogLevelAttribute +{ +} + +class ImplementingInterfaceWithLogLevelAttribute extends \Exception implements InterfaceWithLogLevelAttribute +{ +} From e084246ba2ad7b859d586e1de09f6b24d0cbcb4c Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 20 Dec 2023 10:38:42 +0100 Subject: [PATCH 076/395] [Validator] Add the `Charset` constraint --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + .../Validator/Constraints/Charset.php | 43 ++++++++++ .../Constraints/CharsetValidator.php | 46 ++++++++++ .../Tests/Constraints/CharsetTest.php | 65 ++++++++++++++ .../Constraints/CharsetValidatorTest.php | 86 +++++++++++++++++++ 5 files changed, 241 insertions(+) create mode 100644 src/Symfony/Component/Validator/Constraints/Charset.php create mode 100644 src/Symfony/Component/Validator/Constraints/CharsetValidator.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/CharsetTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index c2c41d6daa4a6..6e65a1355fdaf 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `list` and `associative_array` types to `Type` constraint + * Add the `Charset` constraint 7.0 --- diff --git a/src/Symfony/Component/Validator/Constraints/Charset.php b/src/Symfony/Component/Validator/Constraints/Charset.php new file mode 100644 index 0000000000000..a864a440fec04 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Charset.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; + +/** + * @author Alexandre Daubois + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class Charset extends Constraint +{ + public const BAD_ENCODING_ERROR = '94c5e58b-f892-4e25-8fd6-9d89c80bfe81'; + + protected const ERROR_NAMES = [ + self::BAD_ENCODING_ERROR => 'BAD_ENCODING_ERROR', + ]; + + public array|string $encodings = []; + public string $message = 'The detected encoding "{{ detected }}" does not match one of the accepted encoding: "{{ encodings }}".'; + + public function __construct(array|string $encodings = null, string $message = null, array $groups = null, mixed $payload = null, array $options = null) + { + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->encodings = (array) ($encodings ?? $this->encodings); + + if ([] === $this->encodings) { + throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires at least one encoding.', static::class)); + } + } +} diff --git a/src/Symfony/Component/Validator/Constraints/CharsetValidator.php b/src/Symfony/Component/Validator/Constraints/CharsetValidator.php new file mode 100644 index 0000000000000..2a4ca66a44832 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/CharsetValidator.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @author Alexandre Daubois + */ +final class CharsetValidator extends ConstraintValidator +{ + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof Charset) { + throw new UnexpectedTypeException($constraint, Charset::class); + } + + if (null === $value) { + return; + } + + if (!\is_string($value)) { + throw new UnexpectedValueException($value, 'string'); + } + + if (!\in_array($detected = mb_detect_encoding($value, $constraint->encodings, true), $constraint->encodings, true)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ detected }}', $detected) + ->setParameter('{{ encodings }}', implode('", "', $constraint->encodings)) + ->setCode(Charset::BAD_ENCODING_ERROR) + ->addViolation(); + } + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CharsetTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CharsetTest.php new file mode 100644 index 0000000000000..893066645a94c --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/CharsetTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\Charset; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; + +class CharsetTest extends TestCase +{ + public function testSingleEncodingCanBeSet() + { + $encoding = new Charset('UTF-8'); + + $this->assertSame(['UTF-8'], $encoding->encodings); + } + + public function testMultipleEncodingCanBeSet() + { + $encoding = new Charset(['ASCII', 'UTF-8']); + + $this->assertSame(['ASCII', 'UTF-8'], $encoding->encodings); + } + + public function testThrowsOnNoCharset() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Charset" constraint requires at least one encoding.'); + + new Charset(); + } + + public function testAttributes() + { + $metadata = new ClassMetadata(CharsetDummy::class); + $loader = new AttributeLoader(); + $this->assertTrue($loader->loadClassMetadata($metadata)); + + [$aConstraint] = $metadata->properties['a']->getConstraints(); + $this->assertSame(['UTF-8'], $aConstraint->encodings); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + $this->assertSame(['ASCII', 'UTF-8'], $bConstraint->encodings); + } +} + +class CharsetDummy +{ + #[Charset('UTF-8')] + private string $a; + + #[Charset(['ASCII', 'UTF-8'])] + private string $b; +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php new file mode 100644 index 0000000000000..20a3fe884d25e --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\Charset; +use Symfony\Component\Validator\Constraints\CharsetValidator; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +class CharsetValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): CharsetValidator + { + return new CharsetValidator(); + } + + /** + * @dataProvider provideValidValues + */ + public function testEncodingIsValid(string $value, array $encodings) + { + $this->validator->validate($value, new Charset(encodings: $encodings)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider provideInvalidValues + */ + public function testInvalidValues(string $value, array $encodings) + { + $this->validator->validate($value, new Charset(encodings: $encodings)); + + $this->buildViolation('The detected encoding "{{ detected }}" does not match one of the accepted encoding: "{{ encodings }}".') + ->setParameter('{{ detected }}', mb_detect_encoding($value, $encodings, true)) + ->setParameter('{{ encodings }}', implode(', ', $encodings)) + ->setCode(Charset::BAD_ENCODING_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider provideInvalidTypes + */ + public function testNonStringValues(mixed $value) + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessageMatches('/Expected argument of type "string", ".*" given/'); + + $this->validator->validate($value, new Charset(encodings: ['UTF-8'])); + } + + public static function provideValidValues() + { + yield ['my ascii string', ['ASCII']]; + yield ['my ascii string', ['UTF-8']]; + yield ['my ascii string', ['ASCII', 'UTF-8']]; + yield ['my ûtf 8', ['ASCII', 'UTF-8']]; + yield ['my ûtf 8', ['UTF-8']]; + yield ['ώ', ['UTF-16']]; + } + + public static function provideInvalidValues() + { + yield ['my non-Äscîi string', ['ASCII']]; + yield ['😊', ['7bit']]; + } + + public static function provideInvalidTypes() + { + yield [true]; + yield [false]; + yield [1]; + yield [1.1]; + yield [[]]; + yield [new \stdClass()]; + } +} From 3bbf7fe142ba2f86f8c9893624c4f62914b7b186 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 27 Dec 2023 21:45:32 +0100 Subject: [PATCH 077/395] [Mailer][Postmark] `PostmarkDeliveryEvent::$message` cannot be null --- .../Postmark/Event/PostmarkDeliveryEvent.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEvent.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEvent.php index e20335ad0f8b8..dcde32cf202ef 100644 --- a/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEvent.php +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Event/PostmarkDeliveryEvent.php @@ -17,17 +17,13 @@ class PostmarkDeliveryEvent { public const CODE_INACTIVE_RECIPIENT = 406; - private int $errorCode; - private Headers $headers; - private ?string $message; - - public function __construct(string $message, int $errorCode) + public function __construct( + private string $message, + private int $errorCode, + ) { - $this->message = $message; - $this->errorCode = $errorCode; - $this->headers = new Headers(); } @@ -41,7 +37,7 @@ public function getHeaders(): Headers return $this->headers; } - public function getMessage(): ?string + public function getMessage(): string { return $this->message; } From 51efce1341f23f3d8fe023c207a21c1ebd90c664 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 27 Dec 2023 14:01:16 +0100 Subject: [PATCH 078/395] [Validator] Fix `Charset` validator test data --- .../Validator/Tests/Constraints/CharsetValidatorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php index 20a3fe884d25e..d3a237fdbd777 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php @@ -65,7 +65,7 @@ public static function provideValidValues() yield ['my ascii string', ['ASCII', 'UTF-8']]; yield ['my ûtf 8', ['ASCII', 'UTF-8']]; yield ['my ûtf 8', ['UTF-8']]; - yield ['ώ', ['UTF-16']]; + yield ['string', ['ISO-8859-1']]; } public static function provideInvalidValues() From 4f822d918313c5e6eef3ae74b10db4fd7d9c7b22 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 27 Dec 2023 21:14:42 +0100 Subject: [PATCH 079/395] [Translation] [Bridges] Use CPP --- .../Bridge/Crowdin/CrowdinProvider.php | 23 +++++++------------ .../Bridge/Crowdin/CrowdinProviderFactory.php | 20 ++++++---------- .../Translation/Bridge/Loco/LocoProvider.php | 23 +++++++------------ .../Bridge/Loco/LocoProviderFactory.php | 20 ++++++---------- .../Bridge/Lokalise/LokaliseProvider.php | 20 ++++++---------- .../Lokalise/LokaliseProviderFactory.php | 17 +++++--------- .../Bridge/Phrase/PhraseProvider.php | 12 +++++----- .../Bridge/Phrase/PhraseProviderFactory.php | 4 ++-- .../Translation/Util/ArrayConverter.php | 2 +- 9 files changed, 52 insertions(+), 89 deletions(-) diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php index 4fb3208e527e8..c5d250a1faeaf 100644 --- a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php @@ -31,21 +31,14 @@ */ final class CrowdinProvider implements ProviderInterface { - private HttpClientInterface $client; - private LoaderInterface $loader; - private LoggerInterface $logger; - private XliffFileDumper $xliffFileDumper; - private string $defaultLocale; - private string $endpoint; - - public function __construct(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, XliffFileDumper $xliffFileDumper, string $defaultLocale, string $endpoint) - { - $this->client = $client; - $this->loader = $loader; - $this->logger = $logger; - $this->xliffFileDumper = $xliffFileDumper; - $this->defaultLocale = $defaultLocale; - $this->endpoint = $endpoint; + public function __construct( + private HttpClientInterface $client, + private LoaderInterface $loader, + private LoggerInterface $logger, + private XliffFileDumper $xliffFileDumper, + private string $defaultLocale, + private string $endpoint, + ) { } public function __toString(): string diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProviderFactory.php b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProviderFactory.php index 2b038ed19b79e..7a851c56f9a35 100644 --- a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProviderFactory.php +++ b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProviderFactory.php @@ -27,19 +27,13 @@ final class CrowdinProviderFactory extends AbstractProviderFactory { private const HOST = 'api.crowdin.com'; - private LoaderInterface $loader; - private HttpClientInterface $client; - private LoggerInterface $logger; - private string $defaultLocale; - private XliffFileDumper $xliffFileDumper; - - public function __construct(HttpClientInterface $client, LoggerInterface $logger, string $defaultLocale, LoaderInterface $loader, XliffFileDumper $xliffFileDumper) - { - $this->client = $client; - $this->logger = $logger; - $this->defaultLocale = $defaultLocale; - $this->loader = $loader; - $this->xliffFileDumper = $xliffFileDumper; + public function __construct( + private HttpClientInterface $client, + private LoggerInterface $logger, + private string $defaultLocale, + private LoaderInterface $loader, + private XliffFileDumper $xliffFileDumper + ) { } public function create(Dsn $dsn): CrowdinProvider diff --git a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php index e309c65e2141a..bc3b7d2689937 100644 --- a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php @@ -31,21 +31,14 @@ */ final class LocoProvider implements ProviderInterface { - private HttpClientInterface $client; - private LoaderInterface $loader; - private LoggerInterface $logger; - private string $defaultLocale; - private string $endpoint; - private ?TranslatorBagInterface $translatorBag = null; - - public function __construct(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint, TranslatorBagInterface $translatorBag = null) - { - $this->client = $client; - $this->loader = $loader; - $this->logger = $logger; - $this->defaultLocale = $defaultLocale; - $this->endpoint = $endpoint; - $this->translatorBag = $translatorBag; + public function __construct( + private HttpClientInterface $client, + private LoaderInterface $loader, + private LoggerInterface $logger, + private string $defaultLocale, + private string $endpoint, + private ?TranslatorBagInterface $translatorBag = null, + ) { } public function __toString(): string diff --git a/src/Symfony/Component/Translation/Bridge/Loco/LocoProviderFactory.php b/src/Symfony/Component/Translation/Bridge/Loco/LocoProviderFactory.php index d3943b5a88808..ef710e1a58ce7 100644 --- a/src/Symfony/Component/Translation/Bridge/Loco/LocoProviderFactory.php +++ b/src/Symfony/Component/Translation/Bridge/Loco/LocoProviderFactory.php @@ -26,19 +26,13 @@ final class LocoProviderFactory extends AbstractProviderFactory { private const HOST = 'localise.biz'; - private HttpClientInterface $client; - private LoggerInterface $logger; - private string $defaultLocale; - private LoaderInterface $loader; - private ?TranslatorBagInterface $translatorBag = null; - - public function __construct(HttpClientInterface $client, LoggerInterface $logger, string $defaultLocale, LoaderInterface $loader, TranslatorBagInterface $translatorBag = null) - { - $this->client = $client; - $this->logger = $logger; - $this->defaultLocale = $defaultLocale; - $this->loader = $loader; - $this->translatorBag = $translatorBag; + public function __construct( + private HttpClientInterface $client, + private LoggerInterface $logger, + private string $defaultLocale, + private LoaderInterface $loader, + private ?TranslatorBagInterface $translatorBag = null, + ) { } public function create(Dsn $dsn): LocoProvider diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php index 47cc12f230a28..77d12869f0c67 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php @@ -32,19 +32,13 @@ final class LokaliseProvider implements ProviderInterface { private const LOKALISE_GET_KEYS_LIMIT = 5000; - private HttpClientInterface $client; - private LoaderInterface $loader; - private LoggerInterface $logger; - private string $defaultLocale; - private string $endpoint; - - public function __construct(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint) - { - $this->client = $client; - $this->loader = $loader; - $this->logger = $logger; - $this->defaultLocale = $defaultLocale; - $this->endpoint = $endpoint; + public function __construct( + private HttpClientInterface $client, + private LoaderInterface $loader, + private LoggerInterface $logger, + private string $defaultLocale, + private string $endpoint, + ) { } public function __toString(): string diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProviderFactory.php b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProviderFactory.php index 0b8c3d7c00aa3..fa7f2ba8bdf84 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProviderFactory.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProviderFactory.php @@ -25,17 +25,12 @@ final class LokaliseProviderFactory extends AbstractProviderFactory { private const HOST = 'api.lokalise.com'; - private HttpClientInterface $client; - private LoggerInterface $logger; - private string $defaultLocale; - private LoaderInterface $loader; - - public function __construct(HttpClientInterface $client, LoggerInterface $logger, string $defaultLocale, LoaderInterface $loader) - { - $this->client = $client; - $this->logger = $logger; - $this->defaultLocale = $defaultLocale; - $this->loader = $loader; + public function __construct( + private HttpClientInterface $client, + private LoggerInterface $logger, + private string $defaultLocale, + private LoaderInterface $loader, + ) { } public function create(Dsn $dsn): LokaliseProvider diff --git a/src/Symfony/Component/Translation/Bridge/Phrase/PhraseProvider.php b/src/Symfony/Component/Translation/Bridge/Phrase/PhraseProvider.php index a649d4ba59909..1e6464c0e7575 100644 --- a/src/Symfony/Component/Translation/Bridge/Phrase/PhraseProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Phrase/PhraseProvider.php @@ -32,7 +32,7 @@ class PhraseProvider implements ProviderInterface private array $phraseLocales = []; public function __construct( - private readonly HttpClientInterface $httpClient, + private readonly HttpClientInterface $client, private readonly LoggerInterface $logger, private readonly LoaderInterface $loader, private readonly XliffFileDumper $xliffFileDumper, @@ -71,7 +71,7 @@ public function write(TranslatorBagInterface $translatorBag): void $formData = new FormDataPart($fields); - $response = $this->httpClient->request('POST', 'uploads', [ + $response = $this->client->request('POST', 'uploads', [ 'body' => $formData->bodyToIterable(), 'headers' => $formData->getPreparedHeaders()->toArray(), ]); @@ -109,7 +109,7 @@ public function read(array $domains, array $locales): TranslatorBag $headers = ['If-None-Match' => $cachedResponse['etag']]; } - $response = $this->httpClient->request('GET', 'locales/'.$phraseLocale.'/download', [ + $response = $this->client->request('GET', 'locales/'.$phraseLocale.'/download', [ 'query' => $this->readConfig, 'headers' => $headers, ]); @@ -149,7 +149,7 @@ public function delete(TranslatorBagInterface $translatorBag): void $names = array_map(static fn ($v): ?string => preg_replace('/([\s:,])/', '\\\\\\\\$1', $v), $keys); foreach ($names as $name) { - $response = $this->httpClient->request('DELETE', 'keys', [ + $response = $this->client->request('DELETE', 'keys', [ 'query' => [ 'q' => 'name:'.$name, ], @@ -194,7 +194,7 @@ private function getFallbackLocale(string $locale): ?string private function createLocale(string $locale): void { - $response = $this->httpClient->request('POST', 'locales', [ + $response = $this->client->request('POST', 'locales', [ 'body' => [ 'name' => $locale, 'code' => $locale, @@ -221,7 +221,7 @@ private function initLocales(): void $page = 1; do { - $response = $this->httpClient->request('GET', 'locales', [ + $response = $this->client->request('GET', 'locales', [ 'query' => [ 'per_page' => 100, 'page' => $page, diff --git a/src/Symfony/Component/Translation/Bridge/Phrase/PhraseProviderFactory.php b/src/Symfony/Component/Translation/Bridge/Phrase/PhraseProviderFactory.php index a56b2c26536a5..7466510009897 100644 --- a/src/Symfony/Component/Translation/Bridge/Phrase/PhraseProviderFactory.php +++ b/src/Symfony/Component/Translation/Bridge/Phrase/PhraseProviderFactory.php @@ -41,7 +41,7 @@ class PhraseProviderFactory extends AbstractProviderFactory ]; public function __construct( - private readonly HttpClientInterface $httpClient, + private readonly HttpClientInterface $client, private readonly LoggerInterface $logger, private readonly LoaderInterface $loader, private readonly XliffFileDumper $xliffFileDumper, @@ -62,7 +62,7 @@ public function create(Dsn $dsn): ProviderInterface $endpoint .= ':'.$port; } - $client = $this->httpClient->withOptions([ + $client = $this->client->withOptions([ 'base_uri' => 'https://'.$endpoint.'/v2/projects/'.$this->getUser($dsn).'/', 'headers' => [ 'Authorization' => 'token '.$this->getPassword($dsn), diff --git a/src/Symfony/Component/Translation/Util/ArrayConverter.php b/src/Symfony/Component/Translation/Util/ArrayConverter.php index 64e15b485d72f..2fc666d4aeaf3 100644 --- a/src/Symfony/Component/Translation/Util/ArrayConverter.php +++ b/src/Symfony/Component/Translation/Util/ArrayConverter.php @@ -27,7 +27,7 @@ class ArrayConverter { /** * Converts linear messages array to tree-like array. - * For example this array('foo.bar' => 'value') will be converted to ['foo' => ['bar' => 'value']]. + * For example: ['foo.bar' => 'value'] will be converted to ['foo' => ['bar' => 'value']]. * * @param array $messages Linear messages array */ From 9d5a48e9e6dbdd1ff7373da8bbf26fe833e86c84 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 27 Dec 2023 22:07:59 +0100 Subject: [PATCH 080/395] [ExpressionLanguage] Fix typo --- src/Symfony/Component/ExpressionLanguage/Parser.php | 2 +- .../Component/ExpressionLanguage/SerializedParsedExpression.php | 2 +- src/Symfony/Component/ExpressionLanguage/Token.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/ExpressionLanguage/Parser.php b/src/Symfony/Component/ExpressionLanguage/Parser.php index 8d4ace68d9926..7ac7e62f8bbb0 100644 --- a/src/Symfony/Component/ExpressionLanguage/Parser.php +++ b/src/Symfony/Component/ExpressionLanguage/Parser.php @@ -12,7 +12,7 @@ namespace Symfony\Component\ExpressionLanguage; /** - * Parsers a token stream. + * Parses a token stream. * * This parser implements a "Precedence climbing" algorithm. * diff --git a/src/Symfony/Component/ExpressionLanguage/SerializedParsedExpression.php b/src/Symfony/Component/ExpressionLanguage/SerializedParsedExpression.php index a3f8e73433d00..7ccd20849fe32 100644 --- a/src/Symfony/Component/ExpressionLanguage/SerializedParsedExpression.php +++ b/src/Symfony/Component/ExpressionLanguage/SerializedParsedExpression.php @@ -14,7 +14,7 @@ use Symfony\Component\ExpressionLanguage\Node\Node; /** - * Represents an already parsed expression. + * Represents an already serialized parsed expression. * * @author Fabien Potencier */ diff --git a/src/Symfony/Component/ExpressionLanguage/Token.php b/src/Symfony/Component/ExpressionLanguage/Token.php index b15667b3b17b1..c5196c829141c 100644 --- a/src/Symfony/Component/ExpressionLanguage/Token.php +++ b/src/Symfony/Component/ExpressionLanguage/Token.php @@ -12,7 +12,7 @@ namespace Symfony\Component\ExpressionLanguage; /** - * Represents a Token. + * Represents a token. * * @author Fabien Potencier */ From df568841de18d0fb35d149eb2b69798c439813ff Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 27 Dec 2023 22:18:42 +0100 Subject: [PATCH 081/395] Fix typo --- .../DependencyInjection/CompleteConfigurationTestCase.php | 2 +- .../Component/CssSelector/Tests/XPath/TranslatorTest.php | 4 ++-- .../DependencyInjection/Tests/ContainerBuilderTest.php | 4 ++-- .../Component/DependencyInjection/Tests/ContainerTest.php | 4 ++-- src/Symfony/Component/ErrorHandler/DebugClassLoader.php | 6 +++--- .../Component/ErrorHandler/Tests/DebugClassLoaderTest.php | 2 +- .../Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php | 2 +- .../ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php | 2 +- .../Component/HttpFoundation/Tests/HeaderBagTest.php | 2 +- src/Symfony/Component/Uid/AbstractUid.php | 6 +++--- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php index d9b7bedaf73bc..04fba9fe584d3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php @@ -129,7 +129,7 @@ public function testFirewalls() $configs[] = array_values($configDef->getArguments()); } - // the IDs of the services are case sensitive or insensitive depending on + // the IDs of the services are case-sensitive or insensitive depending on // the Symfony version. Transform them to lowercase to simplify tests. $configs[0][2] = strtolower($configs[0][2]); $configs[2][2] = strtolower($configs[2][2]); diff --git a/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php b/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php index c161b802360de..de15384382652 100644 --- a/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php +++ b/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php @@ -277,7 +277,7 @@ public static function getHtmlIdsTestData() ['div[foobar~="cd"]', []], ['*[lang|="En"]', ['second-li']], ['[lang|="En-us"]', ['second-li']], - // Attribute values are case sensitive + // Attribute values are case-sensitive ['*[lang|="en"]', []], ['[lang|="en-US"]', []], ['*[lang|="e"]', []], @@ -334,7 +334,7 @@ public static function getHtmlIdsTestData() ['* :root', []], ['*:contains("link")', ['html', 'nil', 'outer-div', 'tag-anchor', 'nofollow-anchor']], [':CONtains("link")', ['html', 'nil', 'outer-div', 'tag-anchor', 'nofollow-anchor']], - ['*:contains("LInk")', []], // case sensitive + ['*:contains("LInk")', []], // case-sensitive ['*:contains("e")', ['html', 'nil', 'outer-div', 'first-ol', 'first-li', 'paragraph', 'p-em']], ['*:contains("E")', []], // case-sensitive ['.a', ['first-ol']], diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 9139bb4472f3a..d918782b2dfbf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -1699,8 +1699,8 @@ public function testCaseSensitivity() $container->compile(); - $this->assertNotSame($container->get('foo'), $container->get('fOO'), '->get() returns the service for the given id, case sensitively'); - $this->assertSame($container->get('fOO')->Foo->foo, $container->get('foo'), '->get() returns the service for the given id, case sensitively'); + $this->assertNotSame($container->get('foo'), $container->get('fOO'), '->get() returns the service for the given id, case-sensitively'); + $this->assertSame($container->get('fOO')->Foo->foo, $container->get('foo'), '->get() returns the service for the given id, case-sensitively'); } public function testParameterWithMixedCase() diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php index 2a9b822d0a681..6c1e834a16950 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php @@ -223,8 +223,8 @@ public function testCaseSensitivity() $sc->set('Foo', $foo2 = new \stdClass()); $this->assertSame(['service_container', 'foo', 'Foo'], $sc->getServiceIds()); - $this->assertSame($foo1, $sc->get('foo'), '->get() returns the service for the given id, case sensitively'); - $this->assertSame($foo2, $sc->get('Foo'), '->get() returns the service for the given id, case sensitively'); + $this->assertSame($foo1, $sc->get('foo'), '->get() returns the service for the given id, case-sensitively'); + $this->assertSame($foo2, $sc->get('Foo'), '->get() returns the service for the given id, case-sensitively'); } public function testGetThrowServiceNotFoundException() diff --git a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php index edadb27521e6f..4ca2360adea09 100644 --- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php +++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php @@ -158,13 +158,13 @@ public function __construct(callable $classLoader) $test = realpath($dir.$test); if (false === $test || false === $i) { - // filesystem is case sensitive + // filesystem is case-sensitive self::$caseCheck = 0; } elseif (str_ends_with($test, $file)) { - // filesystem is case insensitive and realpath() normalizes the case of characters + // filesystem is case-insensitive and realpath() normalizes the case of characters self::$caseCheck = 1; } elseif ('Darwin' === \PHP_OS_FAMILY) { - // on MacOSX, HFS+ is case insensitive but realpath() doesn't normalize the case of characters + // on MacOSX, HFS+ is case-insensitive but realpath() doesn't normalize the case of characters self::$caseCheck = 2; } else { // filesystem case checks failed, fallback to disabling them diff --git a/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php index ac08e698e2756..6a910d79f1d2c 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php @@ -87,7 +87,7 @@ public function testFileCaseMismatch() $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Case mismatch between class and real file names'); if (!file_exists(__DIR__.'/Fixtures/CaseMismatch.php')) { - $this->markTestSkipped('Can only be run on case insensitive filesystems'); + $this->markTestSkipped('Can only be run on case-insensitive filesystems'); } class_exists(Fixtures\CaseMismatch::class, true); diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php index 72ee19985e00c..38b042300141d 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php @@ -156,7 +156,7 @@ public function testEnhanceWithFatalError() public function testCannotRedeclareClass() { if (!file_exists(__DIR__.'/../FIXTURES2/REQUIREDTWICE.PHP')) { - $this->markTestSkipped('Can only be run on case insensitive filesystems'); + $this->markTestSkipped('Can only be run on case-insensitive filesystems'); } require_once __DIR__.'/../FIXTURES2/REQUIREDTWICE.PHP'; diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php index 547e33373720b..f9474f7e7f58f 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php @@ -28,7 +28,7 @@ public function testEnhance(string $originalMessage, string $enhancedMessage) $error = $enhancer->enhance(new \Error($originalMessage)); $this->assertInstanceOf(UndefinedFunctionError::class, $error); - // class names are case insensitive and PHP do not return the same + // class names are case-insensitive and PHP do not return the same $this->assertSame(strtolower($enhancedMessage), strtolower($error->getMessage())); $this->assertSame(realpath(__FILE__), $error->getFile()); $this->assertSame($expectedLine, $error->getLine()); diff --git a/src/Symfony/Component/HttpFoundation/Tests/HeaderBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/HeaderBagTest.php index d7507fc03778d..f4a7b77068021 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/HeaderBagTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/HeaderBagTest.php @@ -92,7 +92,7 @@ public function testGet() { $bag = new HeaderBag(['foo' => 'bar', 'fuzz' => 'bizz']); $this->assertEquals('bar', $bag->get('foo'), '->get return current value'); - $this->assertEquals('bar', $bag->get('FoO'), '->get key in case insensitive'); + $this->assertEquals('bar', $bag->get('FoO'), '->get key in case-insensitive'); $this->assertEquals(['bar'], $bag->all('foo'), '->get return the value as array'); // defaults diff --git a/src/Symfony/Component/Uid/AbstractUid.php b/src/Symfony/Component/Uid/AbstractUid.php index 44e84bd9abf11..172f095022700 100644 --- a/src/Symfony/Component/Uid/AbstractUid.php +++ b/src/Symfony/Component/Uid/AbstractUid.php @@ -87,7 +87,7 @@ public static function fromRfc4122(string $uid): static abstract public function toBinary(): string; /** - * Returns the identifier as a base58 case sensitive string. + * Returns the identifier as a base58 case-sensitive string. * * @example 2AifFTC3zXgZzK5fPrrprL (len=22) */ @@ -97,7 +97,7 @@ public function toBase58(): string } /** - * Returns the identifier as a base32 case insensitive string. + * Returns the identifier as a base32 case-insensitive string. * * @see https://tools.ietf.org/html/rfc4648#section-6 * @@ -120,7 +120,7 @@ public function toBase32(): string } /** - * Returns the identifier as a RFC4122 case insensitive string. + * Returns the identifier as a RFC4122 case-insensitive string. * * @see https://tools.ietf.org/html/rfc4122#section-3 * From 1c170f9bdb3e46a09bbbffa66e3ad6982527f09a Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 28 Dec 2023 11:05:25 +0100 Subject: [PATCH 082/395] [Validator] Update `Charset` constraint message --- src/Symfony/Component/Validator/Constraints/Charset.php | 2 +- .../Validator/Tests/Constraints/CharsetValidatorTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Charset.php b/src/Symfony/Component/Validator/Constraints/Charset.php index a864a440fec04..29da9a0766d1f 100644 --- a/src/Symfony/Component/Validator/Constraints/Charset.php +++ b/src/Symfony/Component/Validator/Constraints/Charset.php @@ -27,7 +27,7 @@ final class Charset extends Constraint ]; public array|string $encodings = []; - public string $message = 'The detected encoding "{{ detected }}" does not match one of the accepted encoding: "{{ encodings }}".'; + public string $message = 'The detected character encoding "{{ detected }}" is invalid. Allowed encodings are "{{ encodings }}".'; public function __construct(array|string $encodings = null, string $message = null, array $groups = null, mixed $payload = null, array $options = null) { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php index 20a3fe884d25e..0aa814a8de589 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php @@ -40,7 +40,7 @@ public function testInvalidValues(string $value, array $encodings) { $this->validator->validate($value, new Charset(encodings: $encodings)); - $this->buildViolation('The detected encoding "{{ detected }}" does not match one of the accepted encoding: "{{ encodings }}".') + $this->buildViolation('The detected character encoding "{{ detected }}" is invalid. Allowed encodings are "{{ encodings }}".') ->setParameter('{{ detected }}', mb_detect_encoding($value, $encodings, true)) ->setParameter('{{ encodings }}', implode(', ', $encodings)) ->setCode(Charset::BAD_ENCODING_ERROR) From 7c37f92ec5377c55d130787110a0c7cdd215ff66 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 26 Dec 2023 22:44:28 +0100 Subject: [PATCH 083/395] [Mailer][Notifier] Simplify transport service registration + sorting --- .../Resources/config/mailer_transports.php | 97 ++--- .../Resources/config/notifier_transports.php | 380 +++++------------- .../UnsupportedSchemeExceptionTest.php | 2 +- .../UnsupportedSchemeExceptionTest.php | 8 +- 4 files changed, 129 insertions(+), 358 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php index b8f8227384f9a..f95fc6d640c12 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php @@ -39,73 +39,32 @@ service('http_client')->ignoreOnInvalid(), service('logger')->ignoreOnInvalid(), ]) - ->tag('monolog.logger', ['channel' => 'mailer']) - - ->set('mailer.transport_factory.amazon', SesTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.azure', AzureTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.brevo', BrevoTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.gmail', GmailTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.infobip', InfobipTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.mailersend', MailerSendTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.mailchimp', MandrillTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.mailjet', MailjetTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.mailgun', MailgunTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.mailpace', MailPaceTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.postmark', PostmarkTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.sendgrid', SendgridTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.null', NullTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.scaleway', ScalewayTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.sendmail', SendmailTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.smtp', EsmtpTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory', ['priority' => -100]) - - ->set('mailer.transport_factory.native', NativeTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory'); + ->tag('monolog.logger', ['channel' => 'mailer']); + + $factories = [ + 'amazon' => SesTransportFactory::class, + 'azure' => AzureTransportFactory::class, + 'brevo' => BrevoTransportFactory::class, + 'gmail' => GmailTransportFactory::class, + 'infobip' => InfobipTransportFactory::class, + 'mailchimp' => MandrillTransportFactory::class, + 'mailersend' => MailerSendTransportFactory::class, + 'mailgun' => MailgunTransportFactory::class, + 'mailjet' => MailjetTransportFactory::class, + 'mailpace' => MailPaceTransportFactory::class, + 'native' => NativeTransportFactory::class, + 'null' => NullTransportFactory::class, + 'postmark' => PostmarkTransportFactory::class, + 'scaleway' => ScalewayTransportFactory::class, + 'sendgrid' => SendgridTransportFactory::class, + 'sendmail' => SendmailTransportFactory::class, + 'smtp' => EsmtpTransportFactory::class, + ]; + + foreach ($factories as $name => $class) { + $container->services() + ->set('mailer.transport_factory.'.$name, $class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory'); + } }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index a4a2574a2378e..fbe52abc21335 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -20,156 +20,103 @@ ->set('notifier.transport_factory.abstract', AbstractTransportFactory::class) ->abstract() - ->args([service('event_dispatcher'), service('http_client')->ignoreOnInvalid()]) - - ->set('notifier.transport_factory.bluesky', Bridge\Bluesky\BlueskyTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.brevo', Bridge\Brevo\BrevoTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.slack', Bridge\Slack\SlackTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.linked-in', Bridge\LinkedIn\LinkedInTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.telegram', Bridge\Telegram\TelegramTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.mattermost', Bridge\Mattermost\MattermostTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.vonage', Bridge\Vonage\VonageTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.rocket-chat', Bridge\RocketChat\RocketChatTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.google-chat', Bridge\GoogleChat\GoogleChatTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.twilio', Bridge\Twilio\TwilioTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.twitter', Bridge\Twitter\TwitterTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.unifonic', Bridge\Unifonic\UnifonicTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.all-my-sms', Bridge\AllMySms\AllMySmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.firebase', Bridge\Firebase\FirebaseTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.forty-six-elks', Bridge\FortySixElks\FortySixElksTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.free-mobile', Bridge\FreeMobile\FreeMobileTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.spot-hit', Bridge\SpotHit\SpotHitTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.fake-chat', Bridge\FakeChat\FakeChatTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.fake-sms', Bridge\FakeSms\FakeSmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.ovh-cloud', Bridge\OvhCloud\OvhCloudTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.sinch', Bridge\Sinch\SinchTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.zulip', Bridge\Zulip\ZulipTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.infobip', Bridge\Infobip\InfobipTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.isendpro', Bridge\Isendpro\IsendproTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.mobyt', Bridge\Mobyt\MobytTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.smsapi', Bridge\Smsapi\SmsapiTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.esendex', Bridge\Esendex\EsendexTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.sendberry', Bridge\Sendberry\SendberryTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.iqsms', Bridge\Iqsms\IqsmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.octopush', Bridge\Octopush\OctopushTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.discord', Bridge\Discord\DiscordTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.microsoft-teams', Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.gateway-api', Bridge\GatewayApi\GatewayApiTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.mercure', Bridge\Mercure\MercureTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.gitter', Bridge\Gitter\GitterTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.clickatell', Bridge\Clickatell\ClickatellTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.contact-everyone', Bridge\ContactEveryone\ContactEveryoneTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') + ->args([ + service('event_dispatcher'), + service('http_client')->ignoreOnInvalid(), + ]); + + $chatterFactories = [ + 'google-chat' => Bridge\GoogleChat\GoogleChatTransportFactory::class, + 'telegram' => Bridge\Telegram\TelegramTransportFactory::class, + 'bluesky' => Bridge\Bluesky\BlueskyTransportFactory::class, + 'fake-chat' => Bridge\FakeChat\FakeChatTransportFactory::class, + 'firebase' => Bridge\Firebase\FirebaseTransportFactory::class, + 'gitter' => Bridge\Gitter\GitterTransportFactory::class, + 'line-notify' => Bridge\LineNotify\LineNotifyTransportFactory::class, + 'linked-in' => Bridge\LinkedIn\LinkedInTransportFactory::class, + 'mastodon' => Bridge\Mastodon\MastodonTransportFactory::class, + 'mercure' => Bridge\Mercure\MercureTransportFactory::class, + 'microsoft-teams' => Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory::class, + 'pager-duty' => Bridge\PagerDuty\PagerDutyTransportFactory::class, + 'rocket-chat' => Bridge\RocketChat\RocketChatTransportFactory::class, + 'twitter' => Bridge\Twitter\TwitterTransportFactory::class, + 'zulip' => Bridge\Zulip\ZulipTransportFactory::class, + 'brevo' => Bridge\Brevo\BrevoTransportFactory::class, + 'chatwork' => Bridge\Chatwork\ChatworkTransportFactory::class, + 'discord' => Bridge\Discord\DiscordTransportFactory::class, + 'mattermost' => Bridge\Mattermost\MattermostTransportFactory::class, + 'slack' => Bridge\Slack\SlackTransportFactory::class, + 'zendesk' => Bridge\Zendesk\ZendeskTransportFactory::class, + ]; + + foreach ($chatterFactories as $name => $class) { + $container->services() + ->set('notifier.transport_factory.'.$name, $class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory'); + } + + $texterFactories = [ + 'all-my-sms' => Bridge\AllMySms\AllMySmsTransportFactory::class, + 'bandwidth' => Bridge\Bandwidth\BandwidthTransportFactory::class, + 'click-send' => Bridge\ClickSend\ClickSendTransportFactory::class, + 'clickatell' => Bridge\Clickatell\ClickatellTransportFactory::class, + 'contact-everyone' => Bridge\ContactEveryone\ContactEveryoneTransportFactory::class, + 'engagespot' => Bridge\Engagespot\EngagespotTransportFactory::class, + 'esendex' => Bridge\Esendex\EsendexTransportFactory::class, + 'expo' => Bridge\Expo\ExpoTransportFactory::class, + 'fake-sms' => Bridge\FakeSms\FakeSmsTransportFactory::class, + 'forty-six-elks' => Bridge\FortySixElks\FortySixElksTransportFactory::class, + 'free-mobile' => Bridge\FreeMobile\FreeMobileTransportFactory::class, + 'gateway-api' => Bridge\GatewayApi\GatewayApiTransportFactory::class, + 'go-ip' => Bridge\GoIp\GoIpTransportFactory::class, + 'infobip' => Bridge\Infobip\InfobipTransportFactory::class, + 'iqsms' => Bridge\Iqsms\IqsmsTransportFactory::class, + 'isendpro' => Bridge\Isendpro\IsendproTransportFactory::class, + 'kaz-info-teh' => Bridge\KazInfoTeh\KazInfoTehTransportFactory::class, + 'mailjet' => Bridge\Mailjet\MailjetTransportFactory::class, + 'message-bird' => Bridge\MessageBird\MessageBirdTransportFactory::class, + 'message-media' => Bridge\MessageMedia\MessageMediaTransportFactory::class, + 'mobyt' => Bridge\Mobyt\MobytTransportFactory::class, + 'novu' => Bridge\Novu\NovuTransportFactory::class, + 'ntfy' => Bridge\Ntfy\NtfyTransportFactory::class, + 'octopush' => Bridge\Octopush\OctopushTransportFactory::class, + 'one-signal' => Bridge\OneSignal\OneSignalTransportFactory::class, + 'orange-sms' => Bridge\OrangeSms\OrangeSmsTransportFactory::class, + 'ovh-cloud' => Bridge\OvhCloud\OvhCloudTransportFactory::class, + 'plivo' => Bridge\Plivo\PlivoTransportFactory::class, + 'pushover' => Bridge\Pushover\PushoverTransportFactory::class, + 'redlink' => Bridge\Redlink\RedlinkTransportFactory::class, + 'ring-central' => Bridge\RingCentral\RingCentralTransportFactory::class, + 'sendberry' => Bridge\Sendberry\SendberryTransportFactory::class, + 'simple-textin' => Bridge\SimpleTextin\SimpleTextinTransportFactory::class, + 'sinch' => Bridge\Sinch\SinchTransportFactory::class, + 'sms-factor' => Bridge\SmsFactor\SmsFactorTransportFactory::class, + 'sms77' => Bridge\Sms77\Sms77TransportFactory::class, + 'smsapi' => Bridge\Smsapi\SmsapiTransportFactory::class, + 'smsc' => Bridge\Smsc\SmscTransportFactory::class, + 'smsmode' => Bridge\Smsmode\SmsmodeTransportFactory::class, + 'spot-hit' => Bridge\SpotHit\SpotHitTransportFactory::class, + 'telnyx' => Bridge\Telnyx\TelnyxTransportFactory::class, + 'termii' => Bridge\Termii\TermiiTransportFactory::class, + 'turbo-sms' => Bridge\TurboSms\TurboSmsTransportFactory::class, + 'twilio' => Bridge\Twilio\TwilioTransportFactory::class, + 'unifonic' => Bridge\Unifonic\UnifonicTransportFactory::class, + 'vonage' => Bridge\Vonage\VonageTransportFactory::class, + 'yunpian' => Bridge\Yunpian\YunpianTransportFactory::class, + 'light-sms' => Bridge\LightSms\LightSmsTransportFactory::class, + 'sms-biuras' => Bridge\SmsBiuras\SmsBiurasTransportFactory::class, + 'smsbox' => Bridge\Smsbox\SmsboxTransportFactory::class, + ]; + + foreach ($texterFactories as $name => $class) { + $container->services() + ->set('notifier.transport_factory.'.$name, $class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory'); + } + $container->services() ->set('notifier.transport_factory.amazon-sns', Bridge\AmazonSns\AmazonSnsTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') @@ -179,140 +126,5 @@ ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.light-sms', Bridge\LightSms\LightSmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.sms-biuras', Bridge\SmsBiuras\SmsBiurasTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.smsbox', Bridge\Smsbox\SmsboxTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.smsc', Bridge\Smsc\SmscTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.sms-factor', Bridge\SmsFactor\SmsFactorTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.message-bird', Bridge\MessageBird\MessageBirdTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.message-media', Bridge\MessageMedia\MessageMediaTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.telnyx', Bridge\Telnyx\TelnyxTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.mailjet', Bridge\Mailjet\MailjetTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.yunpian', Bridge\Yunpian\YunpianTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.turbo-sms', Bridge\TurboSms\TurboSmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.sms77', Bridge\Sms77\Sms77TransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.one-signal', Bridge\OneSignal\OneSignalTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.orange-sms', Bridge\OrangeSms\OrangeSmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.expo', Bridge\Expo\ExpoTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.kaz-info-teh', Bridge\KazInfoTeh\KazInfoTehTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.engagespot', Bridge\Engagespot\EngagespotTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.zendesk', Bridge\Zendesk\ZendeskTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.chatwork', Bridge\Chatwork\ChatworkTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.termii', Bridge\Termii\TermiiTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.ring-central', Bridge\RingCentral\RingCentralTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.plivo', Bridge\Plivo\PlivoTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.bandwidth', Bridge\Bandwidth\BandwidthTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.line-notify', Bridge\LineNotify\LineNotifyTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.mastodon', Bridge\Mastodon\MastodonTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.pager-duty', Bridge\PagerDuty\PagerDutyTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.pushover', Bridge\Pushover\PushoverTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.simple-textin', Bridge\SimpleTextin\SimpleTextinTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.click-send', Bridge\ClickSend\ClickSendTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.smsmode', Bridge\Smsmode\SmsmodeTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.novu', Bridge\Novu\NovuTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.ntfy', Bridge\Ntfy\NtfyTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.redlink', Bridge\Redlink\RedlinkTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - ->set('notifier.transport_factory.go-ip', Bridge\GoIp\GoIpTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') ; }; diff --git a/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php index ea99bac56ba33..215eb069c9eb3 100644 --- a/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -40,10 +40,10 @@ public static function setUpBeforeClass(): void BrevoTransportFactory::class => false, GmailTransportFactory::class => false, InfobipTransportFactory::class => false, + MailPaceTransportFactory::class => false, MailerSendTransportFactory::class => false, MailgunTransportFactory::class => false, MailjetTransportFactory::class => false, - MailPaceTransportFactory::class => false, MandrillTransportFactory::class => false, PostmarkTransportFactory::class => false, ScalewayTransportFactory::class => false, diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index 99236f6feecae..38e0a5ef305a3 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -32,8 +32,8 @@ public static function setUpBeforeClass(): void Bridge\Bluesky\BlueskyTransportFactory::class => false, Bridge\Brevo\BrevoTransportFactory::class => false, Bridge\Chatwork\ChatworkTransportFactory::class => false, - Bridge\Clickatell\ClickatellTransportFactory::class => false, Bridge\ClickSend\ClickSendTransportFactory::class => false, + Bridge\Clickatell\ClickatellTransportFactory::class => false, Bridge\ContactEveryone\ContactEveryoneTransportFactory::class => false, Bridge\Discord\DiscordTransportFactory::class => false, Bridge\Engagespot\EngagespotTransportFactory::class => false, @@ -72,19 +72,19 @@ public static function setUpBeforeClass(): void Bridge\PagerDuty\PagerDutyTransportFactory::class => false, Bridge\Plivo\PlivoTransportFactory::class => false, Bridge\Pushover\PushoverTransportFactory::class => false, - Bridge\RingCentral\RingCentralTransportFactory::class => false, Bridge\Redlink\RedlinkTransportFactory::class => false, + Bridge\RingCentral\RingCentralTransportFactory::class => false, Bridge\RocketChat\RocketChatTransportFactory::class => false, Bridge\Sendberry\SendberryTransportFactory::class => false, Bridge\SimpleTextin\SimpleTextinTransportFactory::class => false, Bridge\Sinch\SinchTransportFactory::class => false, Bridge\Slack\SlackTransportFactory::class => false, Bridge\Sms77\Sms77TransportFactory::class => false, - Bridge\Smsapi\SmsapiTransportFactory::class => false, Bridge\SmsBiuras\SmsBiurasTransportFactory::class => false, + Bridge\SmsFactor\SmsFactorTransportFactory::class => false, + Bridge\Smsapi\SmsapiTransportFactory::class => false, Bridge\Smsbox\SmsboxTransportFactory::class => false, Bridge\Smsc\SmscTransportFactory::class => false, - Bridge\SmsFactor\SmsFactorTransportFactory::class => false, Bridge\Smsmode\SmsmodeTransportFactory::class => false, Bridge\SpotHit\SpotHitTransportFactory::class => false, Bridge\Telegram\TelegramTransportFactory::class => false, From 1b4e01e82364875840483bfaf3642163749f681b Mon Sep 17 00:00:00 2001 From: Dennis Fridrich Date: Sat, 9 Dec 2023 17:41:28 +0100 Subject: [PATCH 084/395] Add sms-sluzba.cz Notifier Bridge Fix #52975 --- .../FrameworkExtension.php | 1 + .../Resources/config/notifier_transports.php | 1 + .../Notifier/Bridge/SmsSluzba/.gitattributes | 4 + .../Notifier/Bridge/SmsSluzba/.gitignore | 3 + .../Notifier/Bridge/SmsSluzba/CHANGELOG.md | 7 ++ .../Notifier/Bridge/SmsSluzba/LICENSE | 19 ++++ .../Notifier/Bridge/SmsSluzba/README.md | 23 ++++ .../Bridge/SmsSluzba/SmsSluzbaOptions.php | 44 ++++++++ .../Bridge/SmsSluzba/SmsSluzbaTransport.php | 100 ++++++++++++++++++ .../SmsSluzba/SmsSluzbaTransportFactory.php | 43 ++++++++ .../Tests/SmsSluzbaTransportFactoryTest.php | 48 +++++++++ .../Tests/SmsSluzbaTransportTest.php | 44 ++++++++ .../Notifier/Bridge/SmsSluzba/composer.json | 31 ++++++ .../Bridge/SmsSluzba/phpunit.xml.dist | 31 ++++++ .../Exception/UnsupportedSchemeException.php | 4 + .../UnsupportedSchemeExceptionTest.php | 1 + 16 files changed, 404 insertions(+) create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/.gitattributes create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/.gitignore create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaOptions.php create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/Tests/SmsSluzbaTransportFactoryTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/Tests/SmsSluzbaTransportTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/SmsSluzba/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index f94430eeb0cfb..d1e6aa073ab6d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2754,6 +2754,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\Smsc\SmscTransportFactory::class => 'notifier.transport_factory.smsc', NotifierBridge\SmsFactor\SmsFactorTransportFactory::class => 'notifier.transport_factory.sms-factor', NotifierBridge\Smsmode\SmsmodeTransportFactory::class => 'notifier.transport_factory.smsmode', + NotifierBridge\SmsSluzba\SmsSluzbaTransportFactory::class => 'notifier.transport_factory.sms-sluzba', NotifierBridge\SpotHit\SpotHitTransportFactory::class => 'notifier.transport_factory.spot-hit', NotifierBridge\Telegram\TelegramTransportFactory::class => 'notifier.transport_factory.telegram', NotifierBridge\Telnyx\TelnyxTransportFactory::class => 'notifier.transport_factory.telnyx', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index fbe52abc21335..94962e8f1b9be 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -107,6 +107,7 @@ 'light-sms' => Bridge\LightSms\LightSmsTransportFactory::class, 'sms-biuras' => Bridge\SmsBiuras\SmsBiurasTransportFactory::class, 'smsbox' => Bridge\Smsbox\SmsboxTransportFactory::class, + 'sms-sluzba' => Bridge\SmsSluzba\SmsSluzbaTransportFactory::class, ]; foreach ($texterFactories as $name => $class) { diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/.gitattributes b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/.gitignore b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/CHANGELOG.md new file mode 100644 index 0000000000000..5be39cbeeb951 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.1 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/LICENSE b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/LICENSE new file mode 100644 index 0000000000000..3ed9f412ce53d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/README.md b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/README.md new file mode 100644 index 0000000000000..4519be12bc4ea --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/README.md @@ -0,0 +1,23 @@ +sms-sluzba.cz Notifier +====================== + +Provides [sms-sluzba.cz](https://www.sms-sluzba.cz/) integration for Symfony Notifier. + +DSN example +----------- + +``` +MAILER_DSN=sms-sluzba://USERNAME:PASSWORD@default +``` + +where: + - `USERNAME` is your sms-sluzba.cz login + - `PASSWORD` is your sms-sluzba.cz password + +Resources +--------- + + * [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/Notifier/Bridge/SmsSluzba/SmsSluzbaOptions.php b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaOptions.php new file mode 100644 index 0000000000000..ac310f2cf967f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaOptions.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\SmsSluzba; + +use Symfony\Component\Notifier\Message\MessageOptionsInterface; + +final class SmsSluzbaOptions implements MessageOptionsInterface +{ + public function __construct( + private array $options = [], + ) { + } + + public function toArray(): array + { + return $this->options; + } + + public function getRecipientId(): ?string + { + return null; + } + + /** + * @return $this + */ + public function sendAt(\DateTime $sendAt): static + { + $sendAt->setTimezone(new \DateTimeZone('Europe/Prague')); + + $this->options['send_at'] = $sendAt->format('YmdHis'); + + return $this; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaTransport.php b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaTransport.php new file mode 100644 index 0000000000000..29ecf036918ae --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaTransport.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\SmsSluzba; + +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Component\Serializer\Encoder\XmlEncoder; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Dennis Fridrich + */ +final class SmsSluzbaTransport extends AbstractTransport +{ + protected const HOST = 'smsgateapi.sms-sluzba.cz'; + + public function __construct( + #[\SensitiveParameter] + private string $username, + #[\SensitiveParameter] + private string $password, + HttpClientInterface $client = null, + EventDispatcherInterface $dispatcher = null + ) { + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('sms-sluzba://%s', $this->getEndpoint()); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof SmsMessage && (null === $message->getOptions() || $message->getOptions() instanceof SmsSluzbaOptions); + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof SmsMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, SmsMessage::class, $message); + } + + $endpoint = sprintf( + 'https://%s/apixml30/receiver?login=%s&password=%s', + $this->getEndpoint(), + $this->username, + $this->password + ); + + $options = $message->getOptions()?->toArray() ?? []; + + $response = $this->client->request('POST', $endpoint, [ + 'headers' => [ + 'Content-Type' => 'text/xml', + ], + 'body' => [ + 'outgoing_message' => [ + 'dr_request' => 20, // 0 = delivery report is not required; 20 = delivery report is required + 'recipient' => $message->getPhone(), + 'text' => $message->getSubject(), + 'send_at' => $options['send_at'] ?? null, + ], + ], + ]); + + try { + $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote sms-sluzba.cz server.', $response, 0, $e); + } + + $xmlEncoder = new XmlEncoder(); + $responseXml = $xmlEncoder->decode($response->getContent(), 'xml'); + + if (isset($responseXml['message']) && \is_string($responseXml['message'])) { + throw new TransportException(sprintf('Unable to send the SMS: "%s" (%s).', $responseXml['message'], (int) substr($responseXml['id'], 0, 3)), $response); + } + + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($responseXml['message']['id']); + + return $sentMessage; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaTransportFactory.php new file mode 100644 index 0000000000000..da29b34e934ea --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/SmsSluzbaTransportFactory.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\SmsSluzba; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Dennis Fridrich + */ +final class SmsSluzbaTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): SmsSluzbaTransport + { + $scheme = $dsn->getScheme(); + + if ('sms-sluzba' !== $scheme) { + throw new UnsupportedSchemeException($dsn, 'sms-sluzba', $this->getSupportedSchemes()); + } + + $username = $this->getUser($dsn); + $password = $this->getPassword($dsn); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new SmsSluzbaTransport($username, $password, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['sms-sluzba']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/Tests/SmsSluzbaTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/Tests/SmsSluzbaTransportFactoryTest.php new file mode 100644 index 0000000000000..ab7df84b1c5fa --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/Tests/SmsSluzbaTransportFactoryTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\SmsSluzba\Tests; + +use Symfony\Component\Notifier\Bridge\SmsSluzba\SmsSluzbaTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; + +final class SmsSluzbaTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): SmsSluzbaTransportFactory + { + return new SmsSluzbaTransportFactory(); + } + + public static function createProvider(): iterable + { + yield [ + 'sms-sluzba://host.test', + 'sms-sluzba://username:password@host.test', + ]; + } + + public static function incompleteDsnProvider(): iterable + { + yield 'missing username and password' => ['sms-sluzba://host']; + yield 'missing password' => ['sms-sluzba://username@host']; + } + + public static function supportsProvider(): iterable + { + yield [true, 'sms-sluzba://username:password@default']; + yield [false, 'somethingElse://username:password@default']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://username:password@default']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/Tests/SmsSluzbaTransportTest.php b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/Tests/SmsSluzbaTransportTest.php new file mode 100644 index 0000000000000..2b87384e004dc --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/Tests/SmsSluzbaTransportTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\SmsSluzba\Tests; + +use Symfony\Component\Notifier\Bridge\SmsSluzba\SmsSluzbaTransport; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Transport\DummyMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +final class SmsSluzbaTransportTest extends TransportTestCase +{ + public static function createTransport(HttpClientInterface $client = null, string $from = null): SmsSluzbaTransport + { + return new SmsSluzbaTransport('username', 'password'); + } + + public static function toStringProvider(): iterable + { + yield ['sms-sluzba://smsgateapi.sms-sluzba.cz', self::createTransport()]; + yield ['sms-sluzba://smsgateapi.sms-sluzba.cz', self::createTransport(null, 'TEST')]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('608123456', 'Hello!')]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + yield [new DummyMessage()]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/composer.json b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/composer.json new file mode 100644 index 0000000000000..b9a3cd37a995f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/composer.json @@ -0,0 +1,31 @@ +{ + "name": "symfony/sms-sluzba-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony sms-sluzba.cz Notifier Bridge", + "keywords": ["sms", "sms-sluzba.cz", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Dennis Fridrich", + "email": "fridrich.dennis@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/http-client": "^6.4|^7.1", + "symfony/notifier": "^7.1", + "symfony/serializer": "^6.4|^7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\SmsSluzba\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/SmsSluzba/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/phpunit.xml.dist new file mode 100644 index 0000000000000..960872593a899 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SmsSluzba/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index cef748d006108..7254ecd84e814 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -256,6 +256,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Smsmode\SmsmodeTransportFactory::class, 'package' => 'symfony/smsmode-notifier', ], + 'sms-sluzba' => [ + 'class' => Bridge\SmsSluzba\SmsSluzbaTransportFactory::class, + 'package' => 'symfony/sms-sluzba-notifier', + ], 'sns' => [ 'class' => Bridge\AmazonSns\AmazonSnsTransportFactory::class, 'package' => 'symfony/amazon-sns-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index 38e0a5ef305a3..4c4815756d5b2 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -86,6 +86,7 @@ public static function setUpBeforeClass(): void Bridge\Smsbox\SmsboxTransportFactory::class => false, Bridge\Smsc\SmscTransportFactory::class => false, Bridge\Smsmode\SmsmodeTransportFactory::class => false, + Bridge\SmsSluzba\SmsSluzbaTransportFactory::class => false, Bridge\SpotHit\SpotHitTransportFactory::class => false, Bridge\Telegram\TelegramTransportFactory::class => false, Bridge\Telnyx\TelnyxTransportFactory::class => false, From f78dcd11bfdd3d6341852a2ca17f3c79890f034e Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 28 Dec 2023 13:41:08 +0100 Subject: [PATCH 085/395] [Validator] Improve `Charset` constraint message --- src/Symfony/Component/Validator/Constraints/Charset.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Constraints/Charset.php b/src/Symfony/Component/Validator/Constraints/Charset.php index 29da9a0766d1f..37346b92551c2 100644 --- a/src/Symfony/Component/Validator/Constraints/Charset.php +++ b/src/Symfony/Component/Validator/Constraints/Charset.php @@ -27,7 +27,7 @@ final class Charset extends Constraint ]; public array|string $encodings = []; - public string $message = 'The detected character encoding "{{ detected }}" is invalid. Allowed encodings are "{{ encodings }}".'; + public string $message = 'The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}.'; public function __construct(array|string $encodings = null, string $message = null, array $groups = null, mixed $payload = null, array $options = null) { From 903259cdf337721016a62695d575ab6ce75f8691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 28 Dec 2023 06:54:23 +0100 Subject: [PATCH 086/395] [AssetMapper] Add integrity hash to the default es-module-shims script --- .../ImportMap/ImportMapRenderer.php | 27 +++++++++++++------ .../Tests/ImportMap/ImportMapRendererTest.php | 1 + 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php index e8ad69953cf1c..65957f74e4fd9 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php @@ -27,7 +27,9 @@ */ class ImportMapRenderer { - private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL = 'https://ga.jspm.io/npm:es-module-shims@1.8.0/dist/es-module-shims.js'; + // 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'; public function __construct( private readonly ImportMapGenerator $importMapGenerator, @@ -47,7 +49,7 @@ public function render(string|array $entryPoint, array $attributes = []): string $importMap = []; $modulePreloads = []; $cssLinks = []; - $polyFillPath = null; + $polyfillPath = null; foreach ($importMapData as $importName => $data) { $path = $data['path']; @@ -58,7 +60,7 @@ public function render(string|array $entryPoint, array $attributes = []): string // if this represents the polyfill, hide it from the import map if ($importName === $this->polyfillImportName) { - $polyFillPath = $path; + $polyfillPath = $path; continue; } @@ -102,22 +104,31 @@ public function render(string|array $entryPoint, array $attributes = []): string HTML; - if (false !== $this->polyfillImportName && null === $polyFillPath) { + if (false !== $this->polyfillImportName && null === $polyfillPath) { if ('es-module-shims' !== $this->polyfillImportName) { throw new \InvalidArgumentException(sprintf('The JavaScript module polyfill was not found in your import map. Either disable the polyfill or run "php bin/console importmap:require "%s"" to install it.', $this->polyfillImportName)); } // a fallback for the default polyfill in case it's not in the importmap - $polyFillPath = self::DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL; + $polyfillPath = self::DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL; } - if ($polyFillPath) { - $url = $this->escapeAttributeValue($polyFillPath); + if ($polyfillPath) { + $url = $this->escapeAttributeValue($polyfillPath); + $polyfillAttributes = $scriptAttributes; + + // Add security attributes for the default polyfill hosted on jspm.io + if (self::DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL === $polyfillPath) { + $polyfillAttributes = $this->createAttributesString([ + 'crossorigin' => 'anonymous', + 'integrity' => self::DEFAULT_ES_MODULE_SHIMS_POLYFILL_INTEGRITY, + ] + $attributes); + } $output .= << - + HTML; } diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php index 76a855bf91a1b..0ff4d4069c7d3 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php @@ -121,6 +121,7 @@ public function testDefaultPolyfillUsedIfNotInImportmap() ); $html = $renderer->render(['app']); $this->assertStringContainsString('