diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8da21ec1213f2..e5e58745ee337 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,7 +4,7 @@ | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no -| Tickets | Fix #... +| Tickets | Fix #... | License | MIT | Doc PR | symfony/symfony-docs#... diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 690d04f318a88..a7bca6e521977 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -236,6 +236,25 @@ public function testTypeNotGuessableNoServicesFound() } } + /** + * @requires PHP 8 + */ + public function testTypeNotGuessableUnionType() + { + $this->expectException('Symfony\Component\DependencyInjection\Exception\AutowiringFailedException'); + $this->expectExceptionMessage('Cannot autowire service "a": argument "$collision" of method "Symfony\Component\DependencyInjection\Tests\Compiler\UnionClasses::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionA|Symfony\Component\DependencyInjection\Tests\Compiler\CollisionB" but this class was not found.'); + $container = new ContainerBuilder(); + + $container->register(CollisionA::class); + $container->register(CollisionB::class); + + $aDefinition = $container->register('a', UnionClasses::class); + $aDefinition->setAutowired(true); + + $pass = new AutowirePass(); + $pass->process($container); + } + public function testTypeNotGuessableWithTypeSet() { $container = new ContainerBuilder(); @@ -319,6 +338,40 @@ public function testOptionalParameter() $this->assertEquals(Foo::class, $definition->getArgument(2)); } + /** + * @requires PHP 8 + */ + public function testParameterWithNullUnionIsSkipped() + { + $container = new ContainerBuilder(); + + $optDefinition = $container->register('opt', UnionNull::class); + $optDefinition->setAutowired(true); + + (new AutowirePass())->process($container); + + $definition = $container->getDefinition('opt'); + $this->assertNull($definition->getArgument(0)); + } + + /** + * @requires PHP 8 + */ + public function testParameterWithNullUnionIsAutowired() + { + $container = new ContainerBuilder(); + + $container->register(CollisionInterface::class, CollisionA::class); + + $optDefinition = $container->register('opt', UnionNull::class); + $optDefinition->setAutowired(true); + + (new AutowirePass())->process($container); + + $definition = $container->getDefinition('opt'); + $this->assertEquals(CollisionInterface::class, $definition->getArgument(0)); + } + public function testDontTriggerAutowiring() { $container = new ContainerBuilder(); @@ -435,6 +488,21 @@ public function testScalarArgsCannotBeAutowired() } } + /** + * @requires PHP 8 + */ + public function testUnionScalarArgsCannotBeAutowired() + { + $this->expectException('Symfony\Component\DependencyInjection\Exception\AutowiringFailedException'); + $this->expectExceptionMessage('Cannot autowire service "union_scalars": argument "$timeout" of method "Symfony\Component\DependencyInjection\Tests\Compiler\UnionScalars::__construct()" is type-hinted "int|float", you should configure its value explicitly.'); + $container = new ContainerBuilder(); + + $container->register('union_scalars', UnionScalars::class) + ->setAutowired(true); + + (new AutowirePass())->process($container); + } + public function testNoTypeArgsCannotBeAutowired() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/MergeExtensionConfigurationPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/MergeExtensionConfigurationPassTest.php index d5db48cc30645..a2cb3bda5ed96 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/MergeExtensionConfigurationPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/MergeExtensionConfigurationPassTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Definition\BaseNode; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Resource\FileResource; @@ -128,6 +129,23 @@ public function testThrowingExtensionsGetMergedBag() $this->assertSame(['FOO'], array_keys($container->getParameterBag()->getEnvPlaceholders())); } + + public function testReuseEnvPlaceholderGeneratedByPreviousExtension() + { + if (!property_exists(BaseNode::class, 'placeholderUniquePrefixes')) { + $this->markTestSkipped('This test requires symfony/config ^4.4.11|^5.0.11|^5.1.3'); + } + + $container = new ContainerBuilder(); + $container->registerExtension(new FooExtension()); + $container->registerExtension(new TestCccExtension()); + $container->prependExtensionConfig('foo', ['bool_node' => '%env(bool:MY_ENV_VAR)%']); + $container->prependExtensionConfig('test_ccc', ['bool_node' => '%env(bool:MY_ENV_VAR)%']); + + (new MergeExtensionConfigurationPass())->process($container); + + $this->addToAssertionCount(1); + } } class FooConfiguration implements ConfigurationInterface @@ -139,6 +157,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->scalarNode('bar')->end() ->scalarNode('baz')->end() + ->booleanNode('bool_node')->end() ->end(); return $treeBuilder; @@ -166,6 +185,8 @@ public function load(array $configs, ContainerBuilder $container) $container->getParameterBag()->get('env(BOZ)'); $container->resolveEnvPlaceholders($config['baz']); } + + $container->setParameter('foo.param', 'ccc'); } } @@ -194,3 +215,36 @@ public function load(array $configs, ContainerBuilder $container) throw new \Exception(); } } + +final class TestCccConfiguration implements ConfigurationInterface +{ + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('test_ccc'); + $treeBuilder->getRootNode() + ->children() + ->booleanNode('bool_node')->end() + ->end(); + + return $treeBuilder; + } +} + +final class TestCccExtension extends Extension +{ + public function getAlias(): string + { + return 'test_ccc'; + } + + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface + { + return new TestCccConfiguration(); + } + + public function load(array $configs, ContainerBuilder $container) + { + $configuration = $this->getConfiguration($configs, $container); + $this->processConfiguration($configuration, $configs); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index abb9a9c2ed366..ba664f767baf1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -879,12 +879,12 @@ public function testfindTaggedServiceIds() ->addTag('bar', ['bar' => 'bar']) ->addTag('foo', ['foofoo' => 'foofoo']) ; - $this->assertEquals($builder->findTaggedServiceIds('foo'), [ + $this->assertEquals([ 'foo' => [ ['foo' => 'foo'], ['foofoo' => 'foofoo'], ], - ], '->findTaggedServiceIds() returns an array of service ids and its tag attributes'); + ], $builder->findTaggedServiceIds('foo'), '->findTaggedServiceIds() returns an array of service ids and its tag attributes'); $this->assertEquals([], $builder->findTaggedServiceIds('foobar'), '->findTaggedServiceIds() returns an empty array if there is annotated services'); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php index f67cdd520e709..f5b3a9166be9a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php @@ -280,10 +280,10 @@ public function testTags() $def->addTag('foo', ['foo' => 'bar']); $this->assertEquals([[], ['foo' => 'bar']], $def->getTag('foo'), '->addTag() can adds the same tag several times'); $def->addTag('bar', ['bar' => 'bar']); - $this->assertEquals($def->getTags(), [ + $this->assertEquals([ 'foo' => [[], ['foo' => 'bar']], 'bar' => [['bar' => 'bar']], - ], '->getTags() returns all tags'); + ], $def->getTags(), '->getTags() returns all tags'); } public function testSetArgument() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 51a88705d6fe6..58a186f009097 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -107,7 +107,7 @@ public function testDumpRelativeDir() $container = new ContainerBuilder(); $container->setDefinition('test', $definition); - $container->setParameter('foo', 'wiz'.\dirname(__DIR__)); + $container->setParameter('foo', 'file://'.\dirname(__DIR__)); $container->setParameter('bar', __DIR__); $container->setParameter('baz', '%bar%/PhpDumperTest.php'); $container->setParameter('buz', \dirname(__DIR__, 2)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php index bae07f83ac70d..df5c1c42b2cfc 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php @@ -4,6 +4,10 @@ use Psr\Log\LoggerInterface; +if (PHP_VERSION_ID >= 80000) { + require __DIR__.'/uniontype_classes.php'; +} + class Foo { } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/uniontype_classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/uniontype_classes.php new file mode 100644 index 0000000000000..3a0c77c53941c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/uniontype_classes.php @@ -0,0 +1,24 @@ +services['test'] = new \stdClass(('wiz'.\dirname(__DIR__, 1)), [('wiz'.\dirname(__DIR__, 1)) => (\dirname(__DIR__, 2).'/')]); + return $this->services['test'] = new \stdClass(('file://'.\dirname(__DIR__, 1)), [('file://'.\dirname(__DIR__, 1)) => (\dirname(__DIR__, 2).'/')]); } public function getParameter($name) @@ -109,7 +109,7 @@ private function getDynamicParameter(string $name) protected function getDefaultParameters(): array { return [ - 'foo' => ('wiz'.\dirname(__DIR__, 1)), + 'foo' => ('file://'.\dirname(__DIR__, 1)), 'bar' => __DIR__, 'baz' => (__DIR__.'/PhpDumperTest.php'), 'buz' => \dirname(__DIR__, 2), diff --git a/src/Symfony/Component/DomCrawler/Tests/FormTest.php b/src/Symfony/Component/DomCrawler/Tests/FormTest.php index 88987b05b5a74..db390efccada2 100644 --- a/src/Symfony/Component/DomCrawler/Tests/FormTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/FormTest.php @@ -170,25 +170,28 @@ public function testMultiValuedFields() '); $this->assertEquals( - array_keys($form->all()), - ['foo[2]', 'foo[3]', 'bar[foo][0]', 'bar[foo][foobar]'] + ['foo[2]', 'foo[3]', 'bar[foo][0]', 'bar[foo][foobar]'], + array_keys($form->all()) ); - $this->assertEquals($form->get('foo[2]')->getValue(), 'foo'); - $this->assertEquals($form->get('foo[3]')->getValue(), 'foo'); - $this->assertEquals($form->get('bar[foo][0]')->getValue(), 'foo'); - $this->assertEquals($form->get('bar[foo][foobar]')->getValue(), 'foo'); + $this->assertEquals('foo', $form->get('foo[2]')->getValue()); + $this->assertEquals('foo', $form->get('foo[3]')->getValue()); + $this->assertEquals('foo', $form->get('bar[foo][0]')->getValue()); + $this->assertEquals('foo', $form->get('bar[foo][foobar]')->getValue()); $form['foo[2]'] = 'bar'; $form['foo[3]'] = 'bar'; - $this->assertEquals($form->get('foo[2]')->getValue(), 'bar'); - $this->assertEquals($form->get('foo[3]')->getValue(), 'bar'); + $this->assertEquals('bar', $form->get('foo[2]')->getValue()); + $this->assertEquals('bar', $form->get('foo[3]')->getValue()); $form['bar'] = ['foo' => ['0' => 'bar', 'foobar' => 'foobar']]; - $this->assertEquals($form->get('bar[foo][0]')->getValue(), 'bar'); - $this->assertEquals($form->get('bar[foo][foobar]')->getValue(), 'foobar'); + $this->assertEquals('bar', $form->get('bar[foo][0]')->getValue()); + $this->assertEquals( + 'foobar', + $form->get('bar[foo][foobar]')->getValue() + ); } /** @@ -979,7 +982,7 @@ public function testGetPhpValuesWithEmptyTextarea() $nodes = $dom->getElementsByTagName('form'); $form = new Form($nodes->item(0), 'http://example.com'); - $this->assertEquals($form->getPhpValues(), ['example' => '']); + $this->assertEquals(['example' => ''], $form->getPhpValues()); } public function testGetReturnTypes() diff --git a/src/Symfony/Component/Dotenv/composer.json b/src/Symfony/Component/Dotenv/composer.json index 5520f45397794..ec04c164914c7 100644 --- a/src/Symfony/Component/Dotenv/composer.json +++ b/src/Symfony/Component/Dotenv/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "require-dev": { "symfony/process": "^3.4.2|^4.0|^5.0" diff --git a/src/Symfony/Component/ErrorHandler/ErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorHandler.php index e02a8fc45dced..bad3d0cfddb4f 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorHandler.php @@ -423,18 +423,6 @@ public function handleError(int $type, string $message, string $file, int $line) } $scope = $this->scopedErrors & $type; - if (4 < $numArgs = \func_num_args()) { - $context = $scope ? (func_get_arg(4) ?: []) : []; - } else { - $context = []; - } - - if (isset($context['GLOBALS']) && $scope) { - $e = $context; // Whatever the signature of the method, - unset($e['GLOBALS'], $context); // $context is always a reference in 5.3 - $context = $e; - } - if (false !== strpos($message, "@anonymous\0")) { $logMessage = $this->parseAnonymousClass($message); } else { @@ -496,6 +484,8 @@ public function handleError(int $type, string $message, string $file, int $line) // `return trigger_error($e, E_USER_ERROR);` allows this error handler // to make $e get through the __toString() barrier. + $context = 4 < \func_num_args() ? (func_get_arg(4) ?: []) : []; + foreach ($context as $e) { if ($e instanceof \Throwable && $e->__toString() === $message) { self::$toStringException = $e; diff --git a/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php index 55fd4de29f57d..2b1d181b58d64 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php @@ -175,9 +175,9 @@ public function testPreviousError() $flattened = FlattenException::createFromThrowable($exception)->getPrevious(); - $this->assertEquals($flattened->getMessage(), 'Oh noes!', 'The message is copied from the original exception.'); - $this->assertEquals($flattened->getCode(), 42, 'The code is copied from the original exception.'); - $this->assertEquals($flattened->getClass(), 'ParseError', 'The class is set to the class of the original exception'); + $this->assertEquals('Oh noes!', $flattened->getMessage(), 'The message is copied from the original exception.'); + $this->assertEquals(42, $flattened->getCode(), 'The code is copied from the original exception.'); + $this->assertEquals('ParseError', $flattened->getClass(), 'The class is set to the class of the original exception'); } /** @@ -303,7 +303,7 @@ function () {}, $this->assertSame(['resource', 'stream'], $array[$i++]); $args = $array[$i++]; - $this->assertSame($args[0], 'object'); + $this->assertSame('object', $args[0]); $this->assertTrue('Closure' === $args[1] || is_subclass_of($args[1], '\Closure'), 'Expect object class name to be Closure or a subclass of Closure.'); $this->assertSame(['array', [['integer', 1], ['integer', 2]]], $array[$i++]); @@ -318,8 +318,8 @@ function () {}, $this->assertSame(['float', INF], $array[$i++]); // assertEquals() does not like NAN values. - $this->assertEquals($array[$i][0], 'float'); - $this->assertNan($array[$i++][1]); + $this->assertEquals('float', $array[$i][0]); + $this->assertNan($array[$i][1]); } public function testRecursionInArguments() @@ -358,7 +358,7 @@ public function testTooBigArray() $flattened = FlattenException::createFromThrowable($exception); $trace = $flattened->getTrace(); - $this->assertSame($trace[1]['args'][0], ['array', ['array', '*SKIPPED over 10000 entries*']]); + $this->assertSame(['array', ['array', '*SKIPPED over 10000 entries*']], $trace[1]['args'][0]); $serializeTrace = serialize($trace); diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php index 3ae1136c4c976..4b27d3b23aef4 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php @@ -138,7 +138,7 @@ private function getEventFromTypeDeclaration(ContainerBuilder $container, string || !($r = $container->getReflectionClass($class, false)) || !$r->hasMethod($method) || 1 > ($m = $r->getMethod($method))->getNumberOfParameters() - || !($type = $m->getParameters()[0]->getType()) + || !($type = $m->getParameters()[0]->getType()) instanceof \ReflectionNamedType || $type->isBuiltin() || Event::class === ($name = $type->getName()) || LegacyEvent::class === $name diff --git a/src/Symfony/Component/Finder/composer.json b/src/Symfony/Component/Finder/composer.json index 0b1408c0dfd41..8ab56eab7f466 100644 --- a/src/Symfony/Component/Finder/composer.json +++ b/src/Symfony/Component/Finder/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "autoload": { "psr-4": { "Symfony\\Component\\Finder\\": "" }, diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php index 657d9d63bec26..bc31505157f77 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php @@ -13,6 +13,8 @@ use Symfony\Component\Form\DataMapperInterface; use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyAccess\Exception\AccessException; +use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -46,7 +48,7 @@ public function mapDataToForms($data, $forms) $config = $form->getConfig(); if (!$empty && null !== $propertyPath && $config->getMapped()) { - $form->setData($this->propertyAccessor->getValue($data, $propertyPath)); + $form->setData($this->getPropertyValue($data, $propertyPath)); } else { $form->setData($config->getData()); } @@ -76,16 +78,32 @@ public function mapFormsToData($forms, &$data) $propertyValue = $form->getData(); // If the field is of type DateTimeInterface and the data is the same skip the update to // keep the original object hash - if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->propertyAccessor->getValue($data, $propertyPath)) { + if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->getPropertyValue($data, $propertyPath)) { continue; } // If the data is identical to the value in $data, we are // dealing with a reference - if (!\is_object($data) || !$config->getByReference() || $propertyValue !== $this->propertyAccessor->getValue($data, $propertyPath)) { + if (!\is_object($data) || !$config->getByReference() || $propertyValue !== $this->getPropertyValue($data, $propertyPath)) { $this->propertyAccessor->setValue($data, $propertyPath, $propertyValue); } } } } + + private function getPropertyValue($data, $propertyPath) + { + try { + return $this->propertyAccessor->getValue($data, $propertyPath); + } catch (AccessException $e) { + if (!$e instanceof UninitializedPropertyException + // For versions without UninitializedPropertyException check the exception message + && (class_exists(UninitializedPropertyException::class) || false === strpos($e->getMessage(), 'You should initialize it')) + ) { + throw $e; + } + + return null; + } + } } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php index d86ae70968388..f15aac5fde9db 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -140,11 +140,11 @@ public function transform($value) */ public function reverseTransform($value) { - if (!\is_string($value)) { + if (null !== $value && !\is_string($value)) { throw new TransformationFailedException('Expected a string.'); } - if ('' === $value) { + if (null === $value || '' === $value) { return null; } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 4be88149770f8..dce846f10e61e 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -368,7 +368,7 @@ private function addSubForm(FormBuilderInterface $builder, string $name, ChoiceV 'value' => $choiceView->value, 'label' => $choiceView->label, 'attr' => $choiceView->attr, - 'translation_domain' => $options['translation_domain'], + 'translation_domain' => $options['choice_translation_domain'], 'block_name' => 'entry', ]; diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php index 2ac749dfff01d..1f2dd3aa40f28 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php +++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php @@ -153,7 +153,10 @@ public function validate($form, Constraint $formConstraint) foreach ($form as $child) { if (!$child->isSynchronized()) { $childrenSynchronized = false; - break; + + $fieldFormConstraint = new Form(); + $this->context->setNode($this->context->getValue(), $child, $this->context->getMetadata(), $this->context->getPropertyPath()); + $validator->atPath(sprintf('children[%s]', $child->getName()))->validate($child, $fieldFormConstraint); } } diff --git a/src/Symfony/Component/Form/Extension/Validator/ValidatorTypeGuesser.php b/src/Symfony/Component/Form/Extension/Validator/ValidatorTypeGuesser.php index 4e0443dd3b680..880d300fa4d97 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ValidatorTypeGuesser.php +++ b/src/Symfony/Component/Form/Extension/Validator/ValidatorTypeGuesser.php @@ -97,6 +97,7 @@ public function guessTypeForConstraint(Constraint $constraint) case 'long': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\IntegerType', [], Guess::MEDIUM_CONFIDENCE); + case \DateTime::class: case '\DateTime': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', [], Guess::MEDIUM_CONFIDENCE); diff --git a/src/Symfony/Component/Form/Resources/translations/validators.de.xlf b/src/Symfony/Component/Form/Resources/translations/validators.de.xlf index a9a183197edc6..fe4353120d256 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.de.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.de.xlf @@ -14,6 +14,10 @@ The CSRF token is invalid. Please try to resubmit the form. Der CSRF-Token ist ungültig. Versuchen Sie bitte das Formular erneut zu senden. + + This value is not a valid HTML5 color. + Dieser Wert ist keine gültige HTML5 Farbe. + diff --git a/src/Symfony/Component/Form/Resources/translations/validators.en.xlf b/src/Symfony/Component/Form/Resources/translations/validators.en.xlf index b8542d319ddec..97ed83fd47425 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.en.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.en.xlf @@ -14,6 +14,106 @@ The CSRF token is invalid. Please try to resubmit the form. The CSRF token is invalid. Please try to resubmit the form. + + This value is not a valid HTML5 color. + This value is not a valid HTML5 color. + + + Please enter a valid birthdate. + Please enter a valid birthdate. + + + The selected choice is invalid. + The selected choice is invalid. + + + The collection is invalid. + The collection is invalid. + + + Please select a valid color. + Please select a valid color. + + + Please select a valid country. + Please select a valid country. + + + Please select a valid currency. + Please select a valid currency. + + + Please choose a valid date interval. + Please choose a valid date interval. + + + Please enter a valid date and time. + Please enter a valid date and time. + + + Please enter a valid date. + Please enter a valid date. + + + Please select a valid file. + Please select a valid file. + + + The hidden field is invalid. + The hidden field is invalid. + + + Please enter an integer. + Please enter an integer. + + + Please select a valid language. + Please select a valid language. + + + Please select a valid locale. + Please select a valid locale. + + + Please enter a valid money amount. + Please enter a valid money amount. + + + Please enter a number. + Please enter a number. + + + The password is invalid. + The password is invalid. + + + Please enter a percentage value. + Please enter a percentage value. + + + The values do not match. + The values do not match. + + + Please enter a valid time. + Please enter a valid time. + + + Please select a valid timezone. + Please select a valid timezone. + + + Please enter a valid URL. + Please enter a valid URL. + + + Please enter a valid search term. + Please enter a valid search term. + + + Please provide a valid phone number. + Please provide a valid phone number. + diff --git a/src/Symfony/Component/Form/Resources/translations/validators.fr.xlf b/src/Symfony/Component/Form/Resources/translations/validators.fr.xlf index 21f9010143afc..a32c83fc93026 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.fr.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.fr.xlf @@ -14,6 +14,10 @@ The CSRF token is invalid. Please try to resubmit the form. Le jeton CSRF est invalide. Veuillez renvoyer le formulaire. + + This value is not a valid HTML5 color. + Cette valeur n'est pas une couleur HTML5 valide. + diff --git a/src/Symfony/Component/Form/Resources/translations/validators.tl.xlf b/src/Symfony/Component/Form/Resources/translations/validators.tl.xlf index 02def0bf31f6f..950c4110d880a 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.tl.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.tl.xlf @@ -14,6 +14,106 @@ The CSRF token is invalid. Please try to resubmit the form. Hindi balido ang CSRF token. Maagpasa muli ng isang pang porma. + + This value is not a valid HTML5 color. + Ang halagang ito ay hindi wastong HTML5 color. + + + Please enter a valid birthdate. + Pakilagay ang tamang petsa ng kapanganakan. + + + The selected choice is invalid. + Ang pinagpiliang sagot ay hindi tama. + + + The collection is invalid. + Hindi balido ang koleksyon. + + + Please select a valid color. + Pakipiliin ang nararapat na kulay. + + + Please select a valid country. + Pakipiliin ang nararapat na bansa. + + + Please select a valid currency. + Pakipiliin ang tamang pananalapi. + + + Please choose a valid date interval. + Piliin ang wastong agwat ng petsa. + + + Please enter a valid date and time. + Piliin ang wastong petsa at oras. + + + Please enter a valid date. + Ilagay ang wastong petsa. + + + Please select a valid file. + Piliin ang balidong file. + + + The hidden field is invalid. + Hindi balido ang field na nakatago. + + + Please enter an integer. + Pakilagay ang integer. + + + Please select a valid language. + Piliin ang nararapat na lengguwahe. + + + Please select a valid locale. + Pakipili ang nararapat na locale. + + + Please enter a valid money amount. + Pakilagay ang tamang halaga ng pera. + + + Please enter a number. + Ilagay ang numero. + + + The password is invalid. + Hindi balido ang password. + + + Please enter a percentage value. + Pakilagay ang tamang porsyento ng halaga. + + + The values do not match. + Hindi tugma ang mga halaga. + + + Please enter a valid time. + Pakilagay ang tamang oras. + + + Please select a valid timezone. + Pakilagay ang tamang sona ng oras. + + + Please enter a valid URL. + Pakilagay ang balidong URL. + + + Please enter a valid search term. + Pakilagay ang balidong katagang sinasaliksik. + + + Please provide a valid phone number. + Pakilagay ang balidong numero ng telepono. + diff --git a/src/Symfony/Component/Form/Resources/translations/validators.vi.xlf b/src/Symfony/Component/Form/Resources/translations/validators.vi.xlf index b5b2f83a9a0dd..4d060fbe0d96e 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.vi.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.vi.xlf @@ -4,7 +4,7 @@ This form should not contain extra fields. - Mẫu này không nên chứa trường mở rộng + Mẫu này không nên chứa trường mở rộng. The uploaded file was too large. Please try to upload a smaller file. @@ -14,6 +14,106 @@ The CSRF token is invalid. Please try to resubmit the form. CSRF token không hợp lệ. Vui lòng thử lại. + + This value is not a valid HTML5 color. + Giá trị này không phải là màu HTML5 hợp lệ. + + + Please enter a valid birthdate. + Vui lòng nhập ngày sinh hợp lệ. + + + The selected choice is invalid. + Lựa chọn không hợp lệ. + + + The collection is invalid. + Danh sách không hợp lệ. + + + Please select a valid color. + Vui lòng chọn một màu hợp lệ. + + + Please select a valid country. + Vui lòng chọn đất nước hợp lệ. + + + Please select a valid currency. + Vui lòng chọn tiền tệ hợp lệ. + + + Please choose a valid date interval. + Vui lòng chọn một khoảng thời gian hợp lệ. + + + Please enter a valid date and time. + Vui lòng nhập ngày và thời gian hợp lệ. + + + Please enter a valid date. + Vui lòng nhập ngày hợp lệ. + + + Please select a valid file. + Vui lòng chọn tệp hợp lệ. + + + The hidden field is invalid. + Phạm vi ẩn không hợp lệ. + + + Please enter an integer. + Vui lòng nhập một số nguyên. + + + Please select a valid language. + Vui lòng chọn ngôn ngữ hợp lệ. + + + Please select a valid locale. + Vui lòng chọn miền hợp lệ. + + + Please enter a valid money amount. + Vui lòng nhập một khoảng tiền hợp lệ. + + + Please enter a number. + Vui lòng nhập một con số. + + + The password is invalid. + Mật khẩu không hợp lệ. + + + Please enter a percentage value. + Vui lòng nhập một giá trị phần trăm. + + + The values do not match. + Các giá trị không phù hợp. + + + Please enter a valid time. + Vui lòng nhập thời gian hợp lệ. + + + Please select a valid timezone. + Vui lòng chọn múi giờ hợp lệ. + + + Please enter a valid URL. + Vui lòng nhập một URL hợp lệ. + + + Please enter a valid search term. + Vui lòng nhập chuỗi tìm kiếm hợp lệ. + + + Please provide a valid phone number. + Vui lòng cung cấp số điện thoại hợp lệ. + diff --git a/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php b/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php index 15fdd85ff6657..7e379f8a269b0 100644 --- a/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php +++ b/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php @@ -58,12 +58,14 @@ public function testAddTaggedTypes() $extDefinition = $container->getDefinition('form.extension'); + $locator = $extDefinition->getArgument(0); + $this->assertTrue(!$locator->isPublic() || $locator->isPrivate()); $this->assertEquals( (new Definition(ServiceLocator::class, [[ __CLASS__.'_Type1' => new ServiceClosureArgument(new Reference('my.type1')), __CLASS__.'_Type2' => new ServiceClosureArgument(new Reference('my.type2')), ]]))->addTag('container.service_locator')->setPublic(false), - $extDefinition->getArgument(0) + $locator->setPublic(false) ); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php index 5890ebb8f4744..76d936d9789a6 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormConfigBuilder; +use Symfony\Component\Form\Tests\Fixtures\TypehintedPropertiesCar; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyAccess\PropertyPath; @@ -113,6 +114,23 @@ public function testMapDataToFormsIgnoresUnmapped() $this->assertNull($form->getData()); } + /** + * @requires PHP 7.4 + */ + public function testMapDataToFormsIgnoresUninitializedProperties() + { + $engineForm = new Form(new FormConfigBuilder('engine', null, $this->dispatcher)); + $colorForm = new Form(new FormConfigBuilder('color', null, $this->dispatcher)); + + $car = new TypehintedPropertiesCar(); + $car->engine = 'BMW'; + + $this->mapper->mapDataToForms($car, [$engineForm, $colorForm]); + + $this->assertSame($car->engine, $engineForm->getData()); + $this->assertNull($colorForm->getData()); + } + public function testMapDataToFormsSetsDefaultDataIfPassedDataIsNull() { $default = new \stdClass(); @@ -293,13 +311,28 @@ public function testMapFormsToDataIgnoresDisabled() $config->setPropertyPath($propertyPath); $config->setData($engine); $config->setDisabled(true); - $form = new Form($config); + $form = new SubmittedForm($config); $this->mapper->mapFormsToData([$form], $car); $this->assertSame($initialEngine, $car->engine); } + /** + * @requires PHP 7.4 + */ + public function testMapFormsToUninitializedProperties() + { + $car = new TypehintedPropertiesCar(); + $config = new FormConfigBuilder('engine', null, $this->dispatcher); + $config->setData('BMW'); + $form = new SubmittedForm($config); + + $this->mapper->mapFormsToData([$form], $car); + + $this->assertSame('BMW', $car->engine); + } + /** * @dataProvider provideDate */ @@ -339,7 +372,7 @@ public function isSubmitted(): bool } } -class NotSynchronizedForm extends Form +class NotSynchronizedForm extends SubmittedForm { public function isSynchronized(): bool { diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index e82ab917c590f..547e86fbfb72a 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -1997,6 +1997,24 @@ public function testStripLeadingUnderscoresAndDigitsFromId() $this->assertEquals('_09name', $view->vars['full_name']); } + public function testSubFormTranslationDomain() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'label' => 'label', + 'translation_domain' => 'label_translation_domain', + 'choices' => [ + 'choice1' => true, + 'choice2' => false, + ], + 'choice_translation_domain' => 'choice_translation_domain', + 'expanded' => true, + ])->createView(); + + $this->assertCount(2, $form->children); + $this->assertSame('choice_translation_domain', $form->children[0]->vars['translation_domain']); + $this->assertSame('choice_translation_domain', $form->children[1]->vars['translation_domain']); + } + /** * @dataProvider provideTrimCases */ diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php index 668018b6b5e15..d67dab2f09988 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php @@ -130,6 +130,21 @@ public function testSubmitNullUsesDefaultEmptyData($emptyData = '10', $expectedD $this->assertSame($expectedData, $form->getData()); } + public function testSubmitNullWithEmptyDataSetToNull() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'empty_data' => null, + ]); + $form->submit(null); + + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + $this->assertSame('', $form->getViewData()); + $this->assertNull($form->getNormData()); + $this->assertNull($form->getData()); + } + public function testSubmitNumericInput(): void { $form = $this->factory->create(static::TESTED_TYPE, null, ['input' => 'number']); diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php index 75ab408b3f701..99c2ff75c67bf 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php @@ -13,6 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Validator\ValidatorExtension; @@ -333,6 +336,63 @@ public function testContextIsPopulatedWithFormBeingValidatedUsingGroupSequence() $this->assertCount(0, $violations); } + + public function testSubmitFormChoiceInvalid() + { + $form = $this->formFactory->create(DateType::class, null, [ + 'widget' => 'choice', + 'years' => [2021], + ]); + + $form->submit([ + 'year' => '2020', + 'month' => '13', + 'day' => '13', + ]); + + $this->assertTrue($form->isSubmitted()); + $this->assertFalse($form->isValid()); + $this->assertCount(2, $form->getErrors()); + $this->assertSame('This value is not valid.', $form->getErrors()[0]->getMessage()); + $this->assertSame($form->get('year'), $form->getErrors()[0]->getOrigin()); + $this->assertSame('This value is not valid.', $form->getErrors()[1]->getMessage()); + $this->assertSame($form->get('month'), $form->getErrors()[1]->getOrigin()); + } + + public function testDoNotAddInvalidMessageIfChildFormIsAlreadyNotSynchronized() + { + $formBuilder = $this->formFactory->createBuilder() + ->add('field1') + ->add('field2') + ->addModelTransformer(new CallbackTransformer( + function () { + }, + function () { + throw new TransformationFailedException('This value is invalid.'); + } + )); + $formBuilder->get('field2')->addModelTransformer(new CallbackTransformer( + function () { + }, + function () { + throw new TransformationFailedException('This value is invalid.'); + } + )); + $form = $formBuilder->getForm(); + + $form->submit([ + 'field1' => 'foo', + 'field2' => 'bar', + ]); + + $this->assertTrue($form->isSubmitted()); + $this->assertFalse($form->isValid()); + $this->assertCount(0, $form->getErrors()); + $this->assertTrue($form->get('field1')->isValid()); + $this->assertCount(0, $form->get('field1')->getErrors()); + $this->assertFalse($form->get('field2')->isValid()); + $this->assertCount(1, $form->get('field2')->getErrors()); + } } class Foo diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php index c3709a41ed7a5..a569a81c6e81b 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php @@ -405,40 +405,6 @@ function () { ; } - // https://github.com/symfony/symfony/issues/4359 - public function testDontMarkInvalidIfAnyChildIsNotSynchronized() - { - $object = new \stdClass(); - $object->child = 'bar'; - - $failingTransformer = new CallbackTransformer( - function ($data) { return $data; }, - function () { throw new TransformationFailedException(); } - ); - - $form = $this->getBuilder('name', '\stdClass') - ->setData($object) - ->addViewTransformer($failingTransformer) - ->setCompound(true) - ->setDataMapper(new PropertyPathMapper()) - ->add( - $this->getBuilder('child') - ->addViewTransformer($failingTransformer) - ) - ->getForm(); - - // Launch transformer - $form->submit(['child' => 'foo']); - - $this->assertTrue($form->isSubmitted()); - $this->assertFalse($form->isSynchronized()); - $this->expectNoValidate(); - - $this->validator->validate($form, new Form()); - - $this->assertNoViolation(); - } - public function testHandleGroupSequenceValidationGroups() { $object = new \stdClass(); diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php index d9f5f8bc298b7..6b83a42895a57 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php @@ -12,9 +12,17 @@ namespace Symfony\Component\Form\Tests\Extension\Validator; use PHPUnit\Framework\TestCase; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser; use Symfony\Component\Form\Guess\Guess; +use Symfony\Component\Form\Guess\TypeGuess; use Symfony\Component\Form\Guess\ValueGuess; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\Constraints\IsTrue; @@ -60,6 +68,35 @@ protected function setUp(): void $this->guesser = new ValidatorTypeGuesser($this->metadataFactory); } + /** + * @dataProvider guessTypeProvider + */ + public function testGuessType(Constraint $constraint, TypeGuess $guess) + { + $this->metadata->addPropertyConstraint(self::TEST_PROPERTY, $constraint); + + $this->assertEquals($guess, $this->guesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY)); + } + + public function guessTypeProvider() + { + return [ + [new Type('array'), new TypeGuess(CollectionType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('bool'), new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('boolean'), new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('double'), new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('float'), new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('numeric'), new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('real'), new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('int'), new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('integer'), new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('long'), new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('string'), new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE)], + [new Type(\DateTime::class), new TypeGuess(DateType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type('\DateTime'), new TypeGuess(DateType::class, [], Guess::MEDIUM_CONFIDENCE)], + ]; + } + public function guessRequiredProvider() { $allowEmptyString = property_exists(Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : []; diff --git a/src/Symfony/Component/Form/Tests/Fixtures/TypehintedPropertiesCar.php b/src/Symfony/Component/Form/Tests/Fixtures/TypehintedPropertiesCar.php new file mode 100644 index 0000000000000..6d88c4841ab34 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/TypehintedPropertiesCar.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +class TypehintedPropertiesCar +{ + public ?string $engine; + public ?string $color; +} diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 283fff0c10812..51bbdba4367d2 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -22,7 +22,7 @@ "symfony/options-resolver": "~4.3|^5.0", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", - "symfony/property-access": "^3.4|^4.0|^5.0", + "symfony/property-access": "^3.4.40|^4.4.8|^5.0.8", "symfony/service-contracts": "^1.1|^2" }, "require-dev": { diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 2fcba7902ebef..abc490a839c07 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -13,7 +13,6 @@ use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; -use Psr\Log\LoggerInterface; use Symfony\Component\HttpClient\Exception\InvalidArgumentException; use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\Internal\CurlClientState; @@ -71,7 +70,7 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions); } - $this->multi = $multi = new CurlClientState(); + $this->multi = new CurlClientState(); self::$curlVersion = self::$curlVersion ?? curl_version(); // Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order @@ -95,10 +94,8 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections return; } - $logger = &$this->logger; - - curl_multi_setopt($this->multi->handle, CURLMOPT_PUSHFUNCTION, static function ($parent, $pushed, array $requestHeaders) use ($multi, $maxPendingPushes, &$logger) { - return self::handlePush($parent, $pushed, $requestHeaders, $multi, $maxPendingPushes, $logger); + curl_multi_setopt($this->multi->handle, CURLMOPT_PUSHFUNCTION, function ($parent, $pushed, array $requestHeaders) use ($maxPendingPushes) { + return $this->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes); }); } @@ -141,12 +138,12 @@ public function request(string $method, string $url, array $options = []): Respo CURLOPT_CERTINFO => $options['capture_peer_cert_chain'], ]; - if (\defined('CURL_VERSION_HTTP2') && (CURL_VERSION_HTTP2 & self::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) { - $curlopts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; - } elseif (1.0 === (float) $options['http_version']) { + if (1.0 === (float) $options['http_version']) { $curlopts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; } elseif (1.1 === (float) $options['http_version']) { $curlopts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; + } elseif (\defined('CURL_VERSION_HTTP2') && (CURL_VERSION_HTTP2 & self::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) { + $curlopts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; } if (isset($options['auth_ntlm'])) { @@ -340,7 +337,7 @@ public function reset() $this->multi->dnsCache->evictions = $this->multi->dnsCache->evictions ?: $this->multi->dnsCache->removals; $this->multi->dnsCache->removals = $this->multi->dnsCache->hostnames = []; - if (\is_resource($this->multi->handle)) { + if (\is_resource($this->multi->handle) || $this->multi->handle instanceof \CurlMultiHandle) { if (\defined('CURLMOPT_PUSHFUNCTION')) { curl_multi_setopt($this->multi->handle, CURLMOPT_PUSHFUNCTION, null); } @@ -350,7 +347,7 @@ public function reset() } foreach ($this->multi->openHandles as [$ch]) { - if (\is_resource($ch)) { + if (\is_resource($ch) || $ch instanceof \CurlHandle) { curl_setopt($ch, CURLOPT_VERBOSE, false); } } @@ -361,7 +358,7 @@ public function __destruct() $this->reset(); } - private static function handlePush($parent, $pushed, array $requestHeaders, CurlClientState $multi, int $maxPendingPushes, ?LoggerInterface $logger): int + private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int { $headers = []; $origin = curl_getinfo($parent, CURLINFO_EFFECTIVE_URL); @@ -373,7 +370,7 @@ private static function handlePush($parent, $pushed, array $requestHeaders, Curl } if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path'])) { - $logger && $logger->debug(sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin)); + $this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin)); return CURL_PUSH_DENY; } @@ -384,21 +381,21 @@ private static function handlePush($parent, $pushed, array $requestHeaders, Curl // but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host, // ignoring domains mentioned as alt-name in the certificate for now (same as curl). if (0 !== strpos($origin, $url.'/')) { - $logger && $logger->debug(sprintf('Rejecting pushed response from "%s": server is not authoritative for "%s"', $origin, $url)); + $this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": server is not authoritative for "%s"', $origin, $url)); return CURL_PUSH_DENY; } - if ($maxPendingPushes <= \count($multi->pushedResponses)) { - $fifoUrl = key($multi->pushedResponses); - unset($multi->pushedResponses[$fifoUrl]); - $logger && $logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl)); + if ($maxPendingPushes <= \count($this->multi->pushedResponses)) { + $fifoUrl = key($this->multi->pushedResponses); + unset($this->multi->pushedResponses[$fifoUrl]); + $this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl)); } $url .= $headers[':path'][0]; - $logger && $logger->debug(sprintf('Queueing pushed response: "%s"', $url)); + $this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url)); - $multi->pushedResponses[$url] = new PushedResponse(new CurlResponse($multi, $pushed), $headers, $multi->openHandles[(int) $parent][1] ?? [], $pushed); + $this->multi->pushedResponses[$url] = new PushedResponse(new CurlResponse($this->multi, $pushed), $headers, $this->multi->openHandles[(int) $parent][1] ?? [], $pushed); return CURL_PUSH_OK; } diff --git a/src/Symfony/Component/HttpClient/HttplugClient.php b/src/Symfony/Component/HttpClient/HttplugClient.php index ec00b3234a037..5d691e024d47c 100644 --- a/src/Symfony/Component/HttpClient/HttplugClient.php +++ b/src/Symfony/Component/HttpClient/HttplugClient.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpClient; use GuzzleHttp\Promise\Promise as GuzzlePromise; +use GuzzleHttp\Promise\RejectedPromise; use Http\Client\Exception\NetworkException; use Http\Client\Exception\RequestException; use Http\Client\HttpAsyncClient; @@ -22,7 +23,6 @@ use Http\Message\StreamFactory; use Http\Message\UriFactory; use Http\Promise\Promise; -use Http\Promise\RejectedPromise; use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\Request; use Nyholm\Psr7\Uri; @@ -114,7 +114,7 @@ public function sendAsyncRequest(RequestInterface $request): Promise try { $response = $this->sendPsr7Request($request, true); } catch (NetworkException $e) { - return new RejectedPromise($e); + return new HttplugPromise(new RejectedPromise($e)); } $waitLoop = $this->waitLoop; diff --git a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php index 1c2e6c8eed48d..af2e6869b37b3 100644 --- a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php +++ b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php @@ -20,7 +20,7 @@ */ final class CurlClientState extends ClientState { - /** @var resource */ + /** @var \CurlMultiHandle|resource */ public $handle; /** @var PushedResponse[] */ public $pushedResponses = []; diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index e7de360a8e410..82b0e2c1ebe38 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -36,13 +36,15 @@ final class CurlResponse implements ResponseInterface private $debugBuffer; /** + * @param \CurlHandle|resource|string $ch + * * @internal */ public function __construct(CurlClientState $multi, $ch, array $options = null, LoggerInterface $logger = null, string $method = 'GET', callable $resolveRedirect = null, int $curlVersion = null) { $this->multi = $multi; - if (\is_resource($ch)) { + if (\is_resource($ch) || $ch instanceof \CurlHandle) { $this->handle = $ch; $this->debugBuffer = fopen('php://temp', 'w+'); if (0x074000 === $curlVersion) { @@ -73,7 +75,17 @@ public function __construct(CurlClientState $multi, $ch, array $options = null, } curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int { - return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); + if (0 !== substr_compare($data, "\r\n", -2)) { + return 0; + } + + $len = 0; + + foreach (explode("\r\n", substr($data, 0, -2)) as $data) { + $len += 2 + self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); + } + + return $len; }); if (null === $options) { @@ -316,10 +328,10 @@ private static function parseHeaderLine($ch, string $data, array &$info, array & return \strlen($data); // Ignore HTTP trailers } - if ("\r\n" !== $data) { + if ('' !== $data) { try { // Regular header line: add it to the list - self::addResponseHeaders([substr($data, 0, -2)], $info, $headers); + self::addResponseHeaders([$data], $info, $headers); } catch (TransportException $e) { $multi->handlesActivity[$id][] = null; $multi->handlesActivity[$id][] = $e; @@ -329,7 +341,7 @@ private static function parseHeaderLine($ch, string $data, array &$info, array & if (0 !== strpos($data, 'HTTP/')) { if (0 === stripos($data, 'Location:')) { - $location = trim(substr($data, 9, -2)); + $location = trim(substr($data, 9)); } return \strlen($data); diff --git a/src/Symfony/Component/HttpClient/Response/HttplugPromise.php b/src/Symfony/Component/HttpClient/Response/HttplugPromise.php index 2f98d6e0b92ec..f3806c9e53e2e 100644 --- a/src/Symfony/Component/HttpClient/Response/HttplugPromise.php +++ b/src/Symfony/Component/HttpClient/Response/HttplugPromise.php @@ -54,6 +54,12 @@ public function getState(): string */ public function wait($unwrap = true) { - return $this->promise->wait($unwrap); + $result = $this->promise->wait($unwrap); + + while ($result instanceof HttplugPromiseInterface || $result instanceof GuzzlePromiseInterface) { + $result = $result->wait($unwrap); + } + + return $result; } } diff --git a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php index 8d4fe59ae6872..e5fb0556930b7 100644 --- a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php +++ b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php @@ -50,7 +50,7 @@ trait ResponseTrait 'canceled' => false, ]; - /** @var resource */ + /** @var object|resource */ private $handle; private $id; private $timeout = 0; diff --git a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php index 6a8cadbbc88a8..1f48be5c574c2 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php @@ -11,13 +11,18 @@ namespace Symfony\Component\HttpClient\Tests; +use GuzzleHttp\Promise\FulfilledPromise as GuzzleFulfilledPromise; use Http\Client\Exception\NetworkException; use Http\Client\Exception\RequestException; +use Http\Promise\FulfilledPromise; use Http\Promise\Promise; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\HttplugClient; +use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\NativeHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\Test\TestHttpServer; class HttplugClientTest extends TestCase @@ -152,4 +157,114 @@ public function testRequestException() $this->expectException(RequestException::class); $client->sendRequest($client->createRequest('BAD.METHOD', 'http://localhost:8057')); } + + public function testRetry404() + { + $client = new HttplugClient(new NativeHttpClient()); + + $successCallableCalled = false; + $failureCallableCalled = false; + + $promise = $client + ->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057/404')) + ->then( + function (ResponseInterface $response) use (&$successCallableCalled, $client) { + $this->assertSame(404, $response->getStatusCode()); + $successCallableCalled = true; + + return $client->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057')); + }, + function (\Exception $exception) use (&$failureCallableCalled) { + $failureCallableCalled = true; + + throw $exception; + } + ) + ; + + $response = $promise->wait(true); + + $this->assertTrue($successCallableCalled); + $this->assertFalse($failureCallableCalled); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testRetryNetworkError() + { + $client = new HttplugClient(new NativeHttpClient()); + + $successCallableCalled = false; + $failureCallableCalled = false; + + $promise = $client + ->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057/chunked-broken')) + ->then(function (ResponseInterface $response) use (&$successCallableCalled) { + $successCallableCalled = true; + + return $response; + }, function (\Exception $exception) use (&$failureCallableCalled, $client) { + $this->assertSame(NetworkException::class, \get_class($exception)); + $this->assertSame(TransportException::class, \get_class($exception->getPrevious())); + $failureCallableCalled = true; + + return $client->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057')); + }) + ; + + $response = $promise->wait(true); + + $this->assertFalse($successCallableCalled); + $this->assertTrue($failureCallableCalled); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testRetryEarlierError() + { + $isFirstRequest = true; + $errorMessage = 'Error occurred before making the actual request.'; + + $client = new HttplugClient(new MockHttpClient(function () use (&$isFirstRequest, $errorMessage) { + if ($isFirstRequest) { + $isFirstRequest = false; + throw new TransportException($errorMessage); + } + + return new MockResponse('OK', ['http_code' => 200]); + })); + + $request = $client->createRequest('GET', 'http://test'); + + $successCallableCalled = false; + $failureCallableCalled = false; + + $promise = $client + ->sendAsyncRequest($request) + ->then( + function (ResponseInterface $response) use (&$successCallableCalled) { + $successCallableCalled = true; + + return $response; + }, + function (\Exception $exception) use ($errorMessage, &$failureCallableCalled, $client, $request) { + $this->assertSame(NetworkException::class, \get_class($exception)); + $this->assertSame($errorMessage, $exception->getMessage()); + $failureCallableCalled = true; + + // Ensure arbitrary levels of promises work. + return (new FulfilledPromise(null))->then(function () use ($client, $request) { + return (new GuzzleFulfilledPromise(null))->then(function () use ($client, $request) { + return $client->sendAsyncRequest($request); + }); + }); + } + ) + ; + + $response = $promise->wait(true); + + $this->assertFalse($successCallableCalled); + $this->assertTrue($failureCallableCalled); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('OK', (string) $response->getBody()); + } } diff --git a/src/Symfony/Component/HttpFoundation/File/MimeType/FileinfoMimeTypeGuesser.php b/src/Symfony/Component/HttpFoundation/File/MimeType/FileinfoMimeTypeGuesser.php index 70a01d7aecd0d..648307708a889 100644 --- a/src/Symfony/Component/HttpFoundation/File/MimeType/FileinfoMimeTypeGuesser.php +++ b/src/Symfony/Component/HttpFoundation/File/MimeType/FileinfoMimeTypeGuesser.php @@ -68,7 +68,13 @@ public function guess($path) if (!$finfo = new \finfo(FILEINFO_MIME_TYPE, $this->magicFile)) { return null; } + $mimeType = $finfo->file($path); - return $finfo->file($path); + if ($mimeType && 0 === (\strlen($mimeType) % 2)) { + $mimeStart = substr($mimeType, 0, \strlen($mimeType) >> 1); + $mimeType = $mimeStart.$mimeStart === $mimeType ? $mimeStart : $mimeType; + } + + return $mimeType; } } diff --git a/src/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeExtensionGuesser.php b/src/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeExtensionGuesser.php index 651be070e122b..9b8ac70ad9f9c 100644 --- a/src/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeExtensionGuesser.php +++ b/src/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeExtensionGuesser.php @@ -625,7 +625,7 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface 'audio/basic' => 'au', 'audio/midi' => 'mid', 'audio/mp4' => 'm4a', - 'audio/mpeg' => 'mpga', + 'audio/mpeg' => 'mp3', 'audio/ogg' => 'oga', 'audio/s3m' => 's3m', 'audio/silk' => 'sil', diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/test.docx b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/test.docx new file mode 100644 index 0000000000000..2e86b6fcea03c Binary files /dev/null and b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/test.docx differ diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/MimeType/MimeTypeTest.php b/src/Symfony/Component/HttpFoundation/Tests/File/MimeType/MimeTypeTest.php index c566db7693d49..4b568e5514794 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/File/MimeType/MimeTypeTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/File/MimeType/MimeTypeTest.php @@ -61,6 +61,11 @@ public function testGuessFileWithUnknownExtension() $this->assertEquals('application/octet-stream', MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/.unknownextension')); } + public function testGuessWithDuplicatedFileType() + { + $this->assertSame('application/vnd.openxmlformats-officedocument.wordprocessingml.document', MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/test.docx')); + } + public function testGuessWithIncorrectPath() { $this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php index c766f88a294a8..0d3c37aaadfb8 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php @@ -617,20 +617,20 @@ public function testSetCache() $options = ['etag' => '"whatever"']; $response->setCache($options); - $this->assertEquals($response->getEtag(), '"whatever"'); + $this->assertEquals('"whatever"', $response->getEtag()); $now = $this->createDateTimeNow(); $options = ['last_modified' => $now]; $response->setCache($options); - $this->assertEquals($response->getLastModified()->getTimestamp(), $now->getTimestamp()); + $this->assertEquals($now->getTimestamp(), $response->getLastModified()->getTimestamp()); $options = ['max_age' => 100]; $response->setCache($options); - $this->assertEquals($response->getMaxAge(), 100); + $this->assertEquals(100, $response->getMaxAge()); $options = ['s_maxage' => 200]; $response->setCache($options); - $this->assertEquals($response->getMaxAge(), 200); + $this->assertEquals(200, $response->getMaxAge()); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 0adabc02cbc21..f0e2d4f5040d6 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -86,7 +86,7 @@ public function testRead() ->method('findOne') ->willReturnCallback(function ($criteria) use ($testTimeout) { $this->assertArrayHasKey($this->options['id_field'], $criteria); - $this->assertEquals($criteria[$this->options['id_field']], 'foo'); + $this->assertEquals('foo', $criteria[$this->options['id_field']]); $this->assertArrayHasKey($this->options['expiry_field'], $criteria); $this->assertArrayHasKey('$gte', $criteria[$this->options['expiry_field']]); diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/PhpBridgeSessionStorageTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/PhpBridgeSessionStorageTest.php index b4fad768834ff..b546b958e03da 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/PhpBridgeSessionStorageTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/PhpBridgeSessionStorageTest.php @@ -83,10 +83,10 @@ public function testClear() $_SESSION['drak'] = 'loves symfony'; $storage->getBag('attributes')->set('symfony', 'greatness'); $key = $storage->getBag('attributes')->getStorageKey(); - $this->assertEquals($_SESSION[$key], ['symfony' => 'greatness']); - $this->assertEquals($_SESSION['drak'], 'loves symfony'); + $this->assertEquals(['symfony' => 'greatness'], $_SESSION[$key]); + $this->assertEquals('loves symfony', $_SESSION['drak']); $storage->clear(); - $this->assertEquals($_SESSION[$key], []); - $this->assertEquals($_SESSION['drak'], 'loves symfony'); + $this->assertEquals([], $_SESSION[$key]); + $this->assertEquals('loves symfony', $_SESSION['drak']); } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php index 1cf4aed06a25d..81c90433d72c4 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php @@ -127,7 +127,7 @@ public function testGc() */ public function testValidateId() { - $mock = $this->getMockBuilder(['SessionHandlerInterface', 'SessionUpdateTimestampHandlerInterface'])->getMock(); + $mock = $this->getMockBuilder(TestSessionHandler::class)->getMock(); $mock->expects($this->once()) ->method('validateId'); @@ -142,7 +142,7 @@ public function testValidateId() */ public function testUpdateTimestamp() { - $mock = $this->getMockBuilder(['SessionHandlerInterface', 'SessionUpdateTimestampHandlerInterface'])->getMock(); + $mock = $this->getMockBuilder(TestSessionHandler::class)->getMock(); $mock->expects($this->once()) ->method('updateTimestamp') ->willReturn(false); @@ -156,3 +156,7 @@ public function testUpdateTimestamp() $this->proxy->updateTimestamp('id', 'data'); } } + +abstract class TestSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ +} diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php index 9370174c253a0..05a68229a331a 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php @@ -48,7 +48,7 @@ private function getType(\ReflectionParameter $parameter, \ReflectionFunctionAbs if (!$type = $parameter->getType()) { return null; } - $name = $type->getName(); + $name = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type; if ($function instanceof \ReflectionMethod) { $lcName = strtolower($name); diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/AddAnnotatedClassesToCachePass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/AddAnnotatedClassesToCachePass.php index 70a987ebf45e0..5eb833b51d074 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/AddAnnotatedClassesToCachePass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/AddAnnotatedClassesToCachePass.php @@ -127,10 +127,10 @@ private function patternsToRegexps(array $patterns): array private function matchAnyRegexps(string $class, array $regexps): bool { - $blacklisted = false !== strpos($class, 'Test'); + $isTest = false !== strpos($class, 'Test'); foreach ($regexps as $regex) { - if ($blacklisted && false === strpos($regex, 'Test')) { + if ($isTest && false === strpos($regex, 'Test')) { continue; } diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php index 26c361f754e53..1ca6c9b458e41 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php @@ -99,7 +99,7 @@ public function onControllerArguments(ControllerArgumentsEvent $event) $r = new \ReflectionFunction(\Closure::fromCallable($event->getController())); $r = $r->getParameters()[$k] ?? null; - if ($r && (!$r->hasType() || \in_array($r->getType()->getName(), [FlattenException::class, LegacyFlattenException::class], true))) { + if ($r && (!($r = $r->getType()) instanceof \ReflectionNamedType || \in_array($r->getName(), [FlattenException::class, LegacyFlattenException::class], true))) { $arguments = $event->getArguments(); $arguments[$k] = FlattenException::createFromThrowable($e); $event->setArguments($arguments); diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 25e8202cdb997..2efa73a3751ff 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -76,11 +76,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - const VERSION = '4.4.10'; - const VERSION_ID = 40410; + const VERSION = '4.4.11'; + const VERSION_ID = 40411; const MAJOR_VERSION = 4; const MINOR_VERSION = 4; - const RELEASE_VERSION = 10; + const RELEASE_VERSION = 11; const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '11/2022'; diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/FileProfilerStorageTest.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/FileProfilerStorageTest.php index f088fe044db1c..9007df16f9532 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Profiler/FileProfilerStorageTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/FileProfilerStorageTest.php @@ -205,9 +205,9 @@ public function testStoreTime() $records = $this->storage->find('', '', 3, 'GET', $start, time() + 3 * 60); $this->assertCount(3, $records, '->find() returns all previously added records'); - $this->assertEquals($records[0]['token'], 'time_2', '->find() returns records ordered by time in descendant order'); - $this->assertEquals($records[1]['token'], 'time_1', '->find() returns records ordered by time in descendant order'); - $this->assertEquals($records[2]['token'], 'time_0', '->find() returns records ordered by time in descendant order'); + $this->assertEquals('time_2', $records[0]['token'], '->find() returns records ordered by time in descendant order'); + $this->assertEquals('time_1', $records[1]['token'], '->find() returns records ordered by time in descendant order'); + $this->assertEquals('time_0', $records[2]['token'], '->find() returns records ordered by time in descendant order'); $records = $this->storage->find('', '', 3, 'GET', $start, time() + 2 * 60); $this->assertCount(2, $records, '->find() should return only first two of the previously added records'); diff --git a/src/Symfony/Component/Intl/Data/Generator/CurrencyDataGenerator.php b/src/Symfony/Component/Intl/Data/Generator/CurrencyDataGenerator.php index e7c28bc24f63a..2c9c31b58cc4f 100644 --- a/src/Symfony/Component/Intl/Data/Generator/CurrencyDataGenerator.php +++ b/src/Symfony/Component/Intl/Data/Generator/CurrencyDataGenerator.php @@ -25,7 +25,7 @@ */ class CurrencyDataGenerator extends AbstractDataGenerator { - private static $blacklist = [ + private static $denylist = [ 'XBA' => true, // European Composite Unit 'XBB' => true, // European Monetary Unit 'XBC' => true, // European Unit of Account (XBC) @@ -133,7 +133,7 @@ private function generateSymbolNamePairs(ArrayAccessibleResourceBundle $rootBund $symbolNamePairs = iterator_to_array($rootBundle['Currencies']); // Remove unwanted currencies - $symbolNamePairs = array_diff_key($symbolNamePairs, self::$blacklist); + $symbolNamePairs = array_diff_key($symbolNamePairs, self::$denylist); return $symbolNamePairs; } diff --git a/src/Symfony/Component/Intl/Data/Generator/LanguageDataGenerator.php b/src/Symfony/Component/Intl/Data/Generator/LanguageDataGenerator.php index 79328afcfabef..9546227b85ea0 100644 --- a/src/Symfony/Component/Intl/Data/Generator/LanguageDataGenerator.php +++ b/src/Symfony/Component/Intl/Data/Generator/LanguageDataGenerator.php @@ -83,7 +83,7 @@ class LanguageDataGenerator extends AbstractDataGenerator 'za' => 'zha', 'zh' => 'zho', ]; - private static $blacklist = [ + private static $denylist = [ 'root' => true, // Absolute root language 'mul' => true, // Multiple languages 'mis' => true, // Uncoded language @@ -182,7 +182,7 @@ protected function generateDataForMeta(BundleEntryReaderInterface $reader, strin private static function generateLanguageNames(ArrayAccessibleResourceBundle $localeBundle): array { - return array_diff_key(iterator_to_array($localeBundle['Languages']), self::$blacklist); + return array_diff_key(iterator_to_array($localeBundle['Languages']), self::$denylist); } private function generateAlpha3Codes(array $languageCodes, ArrayAccessibleResourceBundle $metadataBundle): array diff --git a/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php b/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php index 066cc5fb9d427..146475e7549fc 100644 --- a/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php +++ b/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php @@ -40,7 +40,7 @@ class RegionDataGenerator extends AbstractDataGenerator 'YE' => 'YEM', ]; - private static $blacklist = [ + private static $denylist = [ // Exceptional reservations 'AC' => true, // Ascension Island 'CP' => true, // Clipperton Island @@ -69,7 +69,7 @@ class RegionDataGenerator extends AbstractDataGenerator public static function isValidCountryCode($region) { - if (isset(self::$blacklist[$region])) { + if (isset(self::$denylist[$region])) { return false; } diff --git a/src/Symfony/Component/Intl/Data/Generator/ScriptDataGenerator.php b/src/Symfony/Component/Intl/Data/Generator/ScriptDataGenerator.php index 9c262845074d9..be95b4b9fc4d9 100644 --- a/src/Symfony/Component/Intl/Data/Generator/ScriptDataGenerator.php +++ b/src/Symfony/Component/Intl/Data/Generator/ScriptDataGenerator.php @@ -24,7 +24,7 @@ */ class ScriptDataGenerator extends AbstractDataGenerator { - private static $blacklist = [ + private static $denylist = [ 'Zzzz' => true, // Unknown Script ]; @@ -69,7 +69,7 @@ protected function generateDataForLocale(BundleEntryReaderInterface $reader, str // isset() on \ResourceBundle returns true even if the value is null if (isset($localeBundle['Scripts']) && null !== $localeBundle['Scripts']) { $data = [ - 'Names' => array_diff_key(iterator_to_array($localeBundle['Scripts']), self::$blacklist), + 'Names' => array_diff_key(iterator_to_array($localeBundle['Scripts']), self::$denylist), ]; $this->scriptCodes = array_merge($this->scriptCodes, array_keys($data['Names'])); diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php index b25a6eb447c7e..65fef964a46ab 100644 --- a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php @@ -25,9 +25,9 @@ */ class Connection extends AbstractConnection { - private const LDAP_INVALID_CREDENTIALS = '0x31'; - private const LDAP_TIMEOUT = '0x55'; - private const LDAP_ALREADY_EXISTS = '0x44'; + private const LDAP_INVALID_CREDENTIALS = 0x31; + private const LDAP_TIMEOUT = 0x55; + private const LDAP_ALREADY_EXISTS = 0x44; /** @var bool */ private $bound = false; diff --git a/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php b/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php index c37122fbf2af9..95e181afcf3b4 100644 --- a/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php +++ b/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php @@ -94,12 +94,12 @@ public function testLdapQueryScopeBase() $ldap->getConnection()->bind('cn=admin,dc=symfony,dc=com', 'symfony'); $query = $ldap->createQuery('cn=Fabien Potencier,dc=symfony,dc=com', '(objectclass=*)', [ - 'scope' => Query::SCOPE_BASE, + 'scope' => Query::SCOPE_BASE, ]); $result = $query->execute(); $entry = $result[0]; - $this->assertEquals($result->count(), 1); + $this->assertEquals(1, $result->count()); $this->assertEquals(['Fabien Potencier'], $entry->getAttribute('cn')); } @@ -116,8 +116,8 @@ public function testLdapQueryScopeOneLevel() $subtree_count = $ldap->createQuery('ou=Components,dc=symfony,dc=com', '(objectclass=*)')->execute()->count(); $this->assertNotEquals($one_level_result->count(), $subtree_count); - $this->assertEquals($one_level_result->count(), 1); - $this->assertEquals($one_level_result[0]->getAttribute('ou'), ['Ldap']); + $this->assertEquals(1, $one_level_result->count()); + $this->assertEquals(['Ldap'], $one_level_result[0]->getAttribute('ou')); } public function testLdapPagination() diff --git a/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/LdapManagerTest.php b/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/LdapManagerTest.php index fbdcefc15e099..ae45097c066ed 100644 --- a/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/LdapManagerTest.php +++ b/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/LdapManagerTest.php @@ -186,7 +186,7 @@ public function testLdapRename() $result = $this->executeSearchQuery(1); $renamedEntry = $result[0]; - $this->assertEquals($renamedEntry->getAttribute('cn')[0], 'Kevin'); + $this->assertEquals('Kevin', $renamedEntry->getAttribute('cn')[0]); $oldRdn = $entry->getAttribute('cn')[0]; $entryManager->rename($renamedEntry, 'cn='.$oldRdn); diff --git a/src/Symfony/Component/Lock/Store/PdoStore.php b/src/Symfony/Component/Lock/Store/PdoStore.php index a3273c0cbc75d..4de05b4077aa3 100644 --- a/src/Symfony/Component/Lock/Store/PdoStore.php +++ b/src/Symfony/Component/Lock/Store/PdoStore.php @@ -267,7 +267,11 @@ public function createTable(): void $table->setPrimaryKey([$this->idCol]); foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) { - $conn->exec($sql); + if (method_exists($conn, 'executeStatement')) { + $conn->executeStatement($sql); + } else { + $conn->exec($sql); + } } return; @@ -293,7 +297,11 @@ public function createTable(): void throw new \DomainException(sprintf('Creating the lock table is currently not implemented for PDO driver "%s".', $driver)); } - $conn->exec($sql); + if (method_exists($conn, 'executeStatement')) { + $conn->executeStatement($sql); + } else { + $conn->exec($sql); + } } /** @@ -303,7 +311,12 @@ private function prune(): void { $sql = "DELETE FROM $this->table WHERE $this->expirationCol <= {$this->getCurrentTimestampStatement()}"; - $this->getConnection()->exec($sql); + $conn = $this->getConnection(); + if (method_exists($conn, 'executeStatement')) { + $conn->executeStatement($sql); + } else { + $conn->exec($sql); + } } private function getDriver(): string @@ -316,26 +329,36 @@ private function getDriver(): string if ($con instanceof \PDO) { $this->driver = $con->getAttribute(\PDO::ATTR_DRIVER_NAME); } else { - switch ($this->driver = $con->getDriver()->getName()) { - case 'mysqli': - throw new NotSupportedException(sprintf('The store "%s" does not support the mysqli driver, use pdo_mysql instead.', static::class)); - case 'pdo_mysql': - case 'drizzle_pdo_mysql': + $driver = $con->getDriver(); + + switch (true) { + case $driver instanceof \Doctrine\DBAL\Driver\Mysqli\Driver: + throw new \LogicException(sprintf('The adapter "%s" does not support the mysqli driver, use pdo_mysql instead.', static::class)); + + case $driver instanceof \Doctrine\DBAL\Driver\AbstractMySQLDriver: $this->driver = 'mysql'; break; - case 'pdo_sqlite': + case $driver instanceof \Doctrine\DBAL\Driver\PDOSqlite\Driver: + case $driver instanceof \Doctrine\DBAL\Driver\PDO\SQLite\Driver: $this->driver = 'sqlite'; break; - case 'pdo_pgsql': + case $driver instanceof \Doctrine\DBAL\Driver\PDOPgSql\Driver: + case $driver instanceof \Doctrine\DBAL\Driver\PDO\PgSQL\Driver: $this->driver = 'pgsql'; break; - case 'oci8': - case 'pdo_oracle': + case $driver instanceof \Doctrine\DBAL\Driver\OCI8\Driver: + case $driver instanceof \Doctrine\DBAL\Driver\PDOOracle\Driver: + case $driver instanceof \Doctrine\DBAL\Driver\PDO\OCI\Driver: $this->driver = 'oci'; break; - case 'pdo_sqlsrv': + case $driver instanceof \Doctrine\DBAL\Driver\SQLSrv\Driver: + case $driver instanceof \Doctrine\DBAL\Driver\PDOSqlsrv\Driver: + case $driver instanceof \Doctrine\DBAL\Driver\PDO\SQLSrv\Driver: $this->driver = 'sqlsrv'; break; + default: + $this->driver = \get_class($driver); + break; } } diff --git a/src/Symfony/Component/Lock/Store/RedisStore.php b/src/Symfony/Component/Lock/Store/RedisStore.php index 0e9241beccfd2..21d7116766cc8 100644 --- a/src/Symfony/Component/Lock/Store/RedisStore.php +++ b/src/Symfony/Component/Lock/Store/RedisStore.php @@ -33,13 +33,13 @@ class RedisStore implements StoreInterface private $initialTtl; /** - * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface $redisClient - * @param float $initialTtl the expiration delay of locks in seconds + * @param \Redis|\RedisArray|\RedisCluster|RedisProxy|RedisClusterProxy\Predis\ClientInterface $redisClient + * @param float $initialTtl the expiration delay of locks in seconds */ public function __construct($redisClient, float $initialTtl = 300.0) { - if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\ClientInterface && !$redisClient instanceof RedisProxy) { - throw new InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, \is_object($redisClient) ? \get_class($redisClient) : \gettype($redisClient))); + if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\ClientInterface && !$redisClient instanceof RedisProxy && !$redisClient instanceof RedisClusterProxy) { + throw new InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster, RedisProxy, RedisClusterProxy or Predis\ClientInterface, "%s" given.', __METHOD__, \is_object($redisClient) ? \get_class($redisClient) : \gettype($redisClient))); } if ($initialTtl <= 0) { diff --git a/src/Symfony/Component/Lock/Tests/Store/StoreFactoryTest.php b/src/Symfony/Component/Lock/Tests/Store/StoreFactoryTest.php index 4e5f387988117..1d88638d26e5f 100644 --- a/src/Symfony/Component/Lock/Tests/Store/StoreFactoryTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/StoreFactoryTest.php @@ -61,6 +61,9 @@ public function validConnections() } if (class_exists(\Redis::class) && class_exists(AbstractAdapter::class)) { yield ['redis://localhost', RedisStore::class]; + yield ['redis://localhost?lazy=1', RedisStore::class]; + yield ['redis://localhost?redis_cluster=1', RedisStore::class]; + yield ['redis://localhost?redis_cluster=1&lazy=1', RedisStore::class]; } if (class_exists(\PDO::class)) { yield ['sqlite:/tmp/sqlite.db', PdoStore::class]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillHttpTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillHttpTransportTest.php index cfdc30bb78e63..22d0a8c75f65e 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillHttpTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillHttpTransportTest.php @@ -57,8 +57,9 @@ public function testSend() $body = json_decode($options['body'], true); $message = $body['raw_message']; $this->assertSame('KEY', $body['key']); + $this->assertSame('Fabien', $body['from_name']); + $this->assertSame('fabpot@symfony.com', $body['from_email']); $this->assertSame('saif.gmati@symfony.com', $body['to'][0]); - $this->assertSame('Fabien ', $body['from_email']); $this->assertStringContainsString('Subject: Hello!', $message); $this->assertStringContainsString('To: Saif Eddin ', $message); diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php index 0537e6d98710f..1c3846d160210 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php @@ -49,7 +49,8 @@ protected function doSendHttp(SentMessage $message): ResponseInterface 'to' => array_map(function (Address $recipient): string { return $recipient->getAddress(); }, $envelope->getRecipients()), - 'from_email' => $envelope->getSender()->toString(), + 'from_email' => $envelope->getSender()->getAddress(), + 'from_name' => $envelope->getSender()->getName(), 'raw_message' => $message->toString(), ], ]); diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php index ab271d574c796..8bc68036cfa41 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php @@ -166,4 +166,30 @@ public function testCustomHeader() $this->assertArrayHasKey('foo', $payload['headers']); $this->assertEquals('bar', $payload['headers']['foo']); } + + public function testReplyTo() + { + $from = 'from@example.com'; + $to = 'to@example.com'; + $replyTo = 'replyto@example.com'; + $email = new Email(); + $email->from($from) + ->to($to) + ->replyTo($replyTo) + ->text('content'); + $envelope = new Envelope(new Address($from), [new Address($to)]); + + $transport = new SendgridApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(SendgridApiTransport::class, 'getPayload'); + $method->setAccessible(true); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('from', $payload); + $this->assertArrayHasKey('email', $payload['from']); + $this->assertSame($from, $payload['from']['email']); + + $this->assertArrayHasKey('reply_to', $payload); + $this->assertArrayHasKey('email', $payload['reply_to']); + $this->assertSame($replyTo, $payload['reply_to']['email']); + } } diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php index 28eb76d25bf6b..cce616baedcc4 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php @@ -93,11 +93,16 @@ private function getPayload(Email $email, Envelope $envelope): array if ($emails = array_map($addressStringifier, $email->getBcc())) { $personalization['bcc'] = $emails; } + if ($emails = array_map($addressStringifier, $email->getReplyTo())) { + // Email class supports an array of reply-to addresses, + // but SendGrid only supports a single address + $payload['reply_to'] = $emails[0]; + } $payload['personalizations'][] = $personalization; // these headers can't be overwritten according to Sendgrid docs - // see https://developers.pepipost.com/migration-api/new-subpage/email-send + // see https://sendgrid.api-docs.io/v3.0/mail-send/mail-send-errors#-Headers-Errors $headersToBypass = ['x-sg-id', 'x-sg-eid', '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)) { diff --git a/src/Symfony/Component/Mailer/Envelope.php b/src/Symfony/Component/Mailer/Envelope.php index 266d251609ece..d37606b1f9ed5 100644 --- a/src/Symfony/Component/Mailer/Envelope.php +++ b/src/Symfony/Component/Mailer/Envelope.php @@ -44,9 +44,13 @@ public static function create(RawMessage $message): self public function setSender(Address $sender): void { - $this->sender = new Address($sender->getAddress()); + $this->sender = $sender; } + /** + * @return Address Returns a "mailbox" as specified by RFC 2822 + * Must be converted to an "addr-spec" when used as a "MAIL FROM" value in SMTP (use getAddress()) + */ public function getSender(): Address { return $this->sender; diff --git a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php index 33e2fdb09a6af..bfd72f05b51c9 100644 --- a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php +++ b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php @@ -29,8 +29,8 @@ public function testConstructorWithAddressSender() public function testConstructorWithNamedAddressSender() { - $e = new Envelope(new Address('fabien@symfony.com', 'Fabien'), [new Address('thomas@symfony.com')]); - $this->assertEquals(new Address('fabien@symfony.com'), $e->getSender()); + $e = new Envelope($sender = new Address('fabien@symfony.com', 'Fabien'), [new Address('thomas@symfony.com')]); + $this->assertEquals($sender, $e->getSender()); } public function testConstructorWithAddressRecipients() @@ -54,31 +54,31 @@ public function testConstructorWithWrongRecipients() public function testSenderFromHeaders() { $headers = new Headers(); - $headers->addPathHeader('Return-Path', new Address('return@symfony.com', 'return')); - $headers->addMailboxListHeader('To', ['from@symfony.com']); + $headers->addPathHeader('Return-Path', $return = new Address('return@symfony.com', 'return')); + $headers->addMailboxListHeader('To', ['to@symfony.com']); $e = Envelope::create(new Message($headers)); - $this->assertEquals(new Address('return@symfony.com', 'return'), $e->getSender()); + $this->assertEquals($return, $e->getSender()); $headers = new Headers(); - $headers->addMailboxHeader('Sender', new Address('sender@symfony.com', 'sender')); - $headers->addMailboxListHeader('To', ['from@symfony.com']); + $headers->addMailboxHeader('Sender', $sender = new Address('sender@symfony.com', 'sender')); + $headers->addMailboxListHeader('To', ['to@symfony.com']); $e = Envelope::create(new Message($headers)); - $this->assertEquals(new Address('sender@symfony.com', 'sender'), $e->getSender()); + $this->assertEquals($sender, $e->getSender()); $headers = new Headers(); - $headers->addMailboxListHeader('From', [new Address('from@symfony.com', 'from'), 'some@symfony.com']); - $headers->addMailboxListHeader('To', ['from@symfony.com']); + $headers->addMailboxListHeader('From', [$from = new Address('from@symfony.com', 'from'), 'some@symfony.com']); + $headers->addMailboxListHeader('To', ['to@symfony.com']); $e = Envelope::create(new Message($headers)); - $this->assertEquals(new Address('from@symfony.com', 'from'), $e->getSender()); + $this->assertEquals($from, $e->getSender()); } public function testSenderFromHeadersWithoutFrom() { $headers = new Headers(); - $headers->addMailboxListHeader('To', ['from@symfony.com']); + $headers->addMailboxListHeader('To', ['to@symfony.com']); $e = Envelope::create($message = new Message($headers)); - $message->getHeaders()->addMailboxListHeader('From', [new Address('from@symfony.com', 'from')]); - $this->assertEquals(new Address('from@symfony.com', 'from'), $e->getSender()); + $message->getHeaders()->addMailboxListHeader('From', [$from = new Address('from@symfony.com', 'from')]); + $this->assertEquals($from, $e->getSender()); } public function testRecipientsFromHeaders() diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index bf62b4c87e953..ab7771b9aedfc 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -228,11 +228,24 @@ private function guessHandledClasses(\ReflectionClass $handlerClass, string $ser throw new RuntimeException(sprintf('Invalid handler service "%s": argument "$%s" of method "%s::__invoke()" must have a type-hint corresponding to the message class it handles.', $serviceId, $parameters[0]->getName(), $handlerClass->getName())); } + if ($type instanceof \ReflectionUnionType) { + $types = []; + foreach ($type->getTypes() as $type) { + if (!$type->isBuiltin()) { + $types[] = (string) $type; + } + } + + if ($types) { + return $types; + } + } + if ($type->isBuiltin()) { throw new RuntimeException(sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::__invoke()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type)); } - return [$parameters[0]->getType()->getName()]; + return [$type->getName()]; } private function registerReceivers(ContainerBuilder $container, array $busIds) @@ -326,14 +339,17 @@ private function registerBusMiddleware(ContainerBuilder $container, string $busI if ($container->findDefinition($messengerMiddlewareId)->isAbstract()) { $childDefinition = new ChildDefinition($messengerMiddlewareId); $childDefinition->setArguments($arguments); - $container->setDefinition($messengerMiddlewareId = $busId.'.middleware.'.$id, $childDefinition); + if (isset($middlewareReferences[$messengerMiddlewareId = $busId.'.middleware.'.$id])) { + $messengerMiddlewareId .= '.'.ContainerBuilder::hash($arguments); + } + $container->setDefinition($messengerMiddlewareId, $childDefinition); } elseif ($arguments) { throw new RuntimeException(sprintf('Invalid middleware factory "%s": a middleware factory must be an abstract definition.', $id)); } - $middlewareReferences[] = new Reference($messengerMiddlewareId); + $middlewareReferences[$messengerMiddlewareId] = new Reference($messengerMiddlewareId); } - $container->getDefinition($busId)->replaceArgument(0, new IteratorArgument($middlewareReferences)); + $container->getDefinition($busId)->replaceArgument(0, new IteratorArgument(array_values($middlewareReferences))); } } diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index 3ec352970a502..a0956ec0f81d0 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -508,21 +508,35 @@ public function testRegistersMiddlewareFromServices() $container->setParameter($middlewareParameter = $fooBusId.'.middleware', [ ['id' => UselessMiddleware::class], - ['id' => 'middleware_with_factory', 'arguments' => ['index_0' => 'foo', 'bar']], + ['id' => 'middleware_with_factory', 'arguments' => $factoryChildMiddlewareArgs1 = ['index_0' => 'foo', 'bar']], + ['id' => 'middleware_with_factory', 'arguments' => $factoryChildMiddlewareArgs2 = ['index_0' => 'baz']], ['id' => 'middleware_with_factory_using_default'], ]); (new MessengerPass())->process($container); (new ResolveChildDefinitionsPass())->process($container); - $this->assertTrue($container->hasDefinition($factoryChildMiddlewareId = $fooBusId.'.middleware.middleware_with_factory')); + $this->assertTrue($container->hasDefinition( + $factoryChildMiddlewareArgs1Id = $fooBusId.'.middleware.middleware_with_factory' + )); $this->assertEquals( ['foo', 'bar'], - $container->getDefinition($factoryChildMiddlewareId)->getArguments(), + $container->getDefinition($factoryChildMiddlewareArgs1Id)->getArguments(), 'parent default argument is overridden, and next ones appended' ); - $this->assertTrue($container->hasDefinition($factoryWithDefaultChildMiddlewareId = $fooBusId.'.middleware.middleware_with_factory_using_default')); + $this->assertTrue($container->hasDefinition( + $factoryChildMiddlewareArgs2Id = $fooBusId.'.middleware.middleware_with_factory.'.ContainerBuilder::hash($factoryChildMiddlewareArgs2) + )); + $this->assertEquals( + ['baz'], + $container->getDefinition($factoryChildMiddlewareArgs2Id)->getArguments(), + 'parent default argument is overridden, and next ones appended' + ); + + $this->assertTrue($container->hasDefinition( + $factoryWithDefaultChildMiddlewareId = $fooBusId.'.middleware.middleware_with_factory_using_default' + )); $this->assertEquals( ['some_default'], $container->getDefinition($factoryWithDefaultChildMiddlewareId)->getArguments(), @@ -531,7 +545,8 @@ public function testRegistersMiddlewareFromServices() $this->assertEquals([ new Reference(UselessMiddleware::class), - new Reference($factoryChildMiddlewareId), + new Reference($factoryChildMiddlewareArgs1Id), + new Reference($factoryChildMiddlewareArgs2Id), new Reference($factoryWithDefaultChildMiddlewareId), ], $container->getDefinition($fooBusId)->getArgument(0)->getValues()); $this->assertFalse($container->hasParameter($middlewareParameter)); diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php index b4348d4958a48..c0204cdf4b270 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php @@ -209,7 +209,7 @@ public function buildConfigurationProvider(): iterable 'expectedAutoSetup' => false, ]; - yield 'options from options array wins over options from dsn' => [ + yield 'options from dsn array wins over options from options' => [ 'dsn' => 'doctrine://default?table_name=name_from_dsn&redeliver_timeout=1200&queue_name=normal&auto_setup=true', 'options' => [ 'table_name' => 'name_from_options', @@ -218,10 +218,10 @@ public function buildConfigurationProvider(): iterable 'auto_setup' => false, ], 'expectedConnection' => 'default', - 'expectedTableName' => 'name_from_options', - 'expectedRedeliverTimeout' => 1800, - 'expectedQueue' => 'important', - 'expectedAutoSetup' => false, + 'expectedTableName' => 'name_from_dsn', + 'expectedRedeliverTimeout' => 1200, + 'expectedQueue' => 'normal', + 'expectedAutoSetup' => true, ]; yield 'options from dsn with falsey boolean' => [ diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineReceiverTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineReceiverTest.php index 45e4dd3b91ce3..cf84a36006e72 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineReceiverTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineReceiverTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Messenger\Tests\Transport\Doctrine; +use Doctrine\DBAL\Driver\PDO\Exception; use Doctrine\DBAL\Driver\PDOException; use Doctrine\DBAL\Exception\DeadlockException; use PHPUnit\Framework\TestCase; @@ -75,7 +76,7 @@ public function testOccursRetryableExceptionFromConnection() { $serializer = $this->createSerializer(); $connection = $this->createMock(Connection::class); - $driverException = new PDOException(new \PDOException('Deadlock', 40001)); + $driverException = class_exists(Exception::class) ? Exception::new(new \PDOException('Deadlock', 40001)) : new PDOException(new \PDOException('Deadlock', 40001)); $connection->method('get')->willThrowException(new DeadlockException('Deadlock', $driverException)); $receiver = new DoctrineReceiver($connection, $serializer); $this->assertSame([], $receiver->get()); diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php b/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php index 19141bd8d94c3..2d4b9bd673e73 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php @@ -85,7 +85,7 @@ public static function buildConfiguration(string $dsn, array $options = []): arr } $configuration = ['connection' => $components['host']]; - $configuration += $options + $query + self::DEFAULT_OPTIONS; + $configuration += $query + $options + self::DEFAULT_OPTIONS; $configuration['auto_setup'] = filter_var($configuration['auto_setup'], FILTER_VALIDATE_BOOLEAN); @@ -126,7 +126,7 @@ public function send(string $body, array $headers, int $delay = 0): string 'available_at' => '?', ]); - $this->executeQuery($queryBuilder->getSQL(), [ + $this->executeStatement($queryBuilder->getSQL(), [ $body, json_encode($headers), $this->configuration['queue_name'], @@ -179,7 +179,7 @@ public function get(): ?array ->set('delivered_at', '?') ->where('id = ?'); $now = new \DateTime(); - $this->executeQuery($queryBuilder->getSQL(), [ + $this->executeStatement($queryBuilder->getSQL(), [ $now, $doctrineEnvelope['id'], ], [ @@ -329,6 +329,33 @@ private function executeQuery(string $sql, array $parameters = [], array $types return $stmt; } + private function executeStatement(string $sql, array $parameters = [], array $types = []) + { + try { + if (method_exists($this->driverConnection, 'executeStatement')) { + $stmt = $this->driverConnection->executeStatement($sql, $parameters, $types); + } else { + $stmt = $this->driverConnection->executeUpdate($sql, $parameters, $types); + } + } catch (TableNotFoundException $e) { + if ($this->driverConnection->isTransactionActive()) { + throw $e; + } + + // create table + if ($this->autoSetup) { + $this->setup(); + } + if (method_exists($this->driverConnection, 'executeStatement')) { + $stmt = $this->driverConnection->executeStatement($sql, $parameters, $types); + } else { + $stmt = $this->driverConnection->executeUpdate($sql, $parameters, $types); + } + } + + return $stmt; + } + private function getSchema(): Schema { $schema = new Schema([], [], $this->driverConnection->getSchemaManager()->createSchemaConfig()); diff --git a/src/Symfony/Component/Messenger/composer.json b/src/Symfony/Component/Messenger/composer.json index bbc5e5e3ff053..877049a95de69 100644 --- a/src/Symfony/Component/Messenger/composer.json +++ b/src/Symfony/Component/Messenger/composer.json @@ -22,8 +22,8 @@ }, "require-dev": { "doctrine/dbal": "^2.6|^3.0", + "doctrine/persistence": "^1.3|^2", "psr/cache": "~1.0", - "doctrine/persistence": "^1.3", "symfony/console": "^3.4|^4.0|^5.0", "symfony/dependency-injection": "^3.4.19|^4.1.8|^5.0", "symfony/event-dispatcher": "^4.3|^5.0", diff --git a/src/Symfony/Component/Mime/FileinfoMimeTypeGuesser.php b/src/Symfony/Component/Mime/FileinfoMimeTypeGuesser.php index b91a4ffeac779..3028159858d1a 100644 --- a/src/Symfony/Component/Mime/FileinfoMimeTypeGuesser.php +++ b/src/Symfony/Component/Mime/FileinfoMimeTypeGuesser.php @@ -57,7 +57,13 @@ public function guessMimeType(string $path): ?string if (false === $finfo = new \finfo(FILEINFO_MIME_TYPE, $this->magicFile)) { return null; } + $mimeType = $finfo->file($path); - return $finfo->file($path); + if ($mimeType && 0 === (\strlen($mimeType) % 2)) { + $mimeStart = substr($mimeType, 0, \strlen($mimeType) >> 1); + $mimeType = $mimeStart.$mimeStart === $mimeType ? $mimeStart : $mimeType; + } + + return $mimeType; } } diff --git a/src/Symfony/Component/Mime/Header/ParameterizedHeader.php b/src/Symfony/Component/Mime/Header/ParameterizedHeader.php index d8e50011fa2ac..9d3b905a28c16 100644 --- a/src/Symfony/Component/Mime/Header/ParameterizedHeader.php +++ b/src/Symfony/Component/Mime/Header/ParameterizedHeader.php @@ -158,7 +158,8 @@ private function createParameter(string $name, string $value): string */ private function getEndOfParameterValue(string $value, bool $encoded = false, bool $firstLine = false): string { - if (!preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) { + $forceHttpQuoting = 'content-disposition' === strtolower($this->getName()) && 'form-data' === $this->getValue(); + if ($forceHttpQuoting || !preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) { $value = '"'.$value.'"'; } $prepend = '='; diff --git a/src/Symfony/Component/Mime/MimeTypes.php b/src/Symfony/Component/Mime/MimeTypes.php index 398e021482a09..653a36458c92b 100644 --- a/src/Symfony/Component/Mime/MimeTypes.php +++ b/src/Symfony/Component/Mime/MimeTypes.php @@ -137,7 +137,7 @@ public function guessMimeType(string $path): ?string } if (!$this->isGuesserSupported()) { - throw new LogicException('Unable to guess the MIME type as no guessers are available (have you enable the php_fileinfo extension?).'); + throw new LogicException('Unable to guess the MIME type as no guessers are available (have you enabled the php_fileinfo extension?).'); } return null; @@ -1113,7 +1113,7 @@ public function guessMimeType(string $path): ?string 'audio/mp2' => ['mp2'], 'audio/mp3' => ['mp3', 'mpga'], 'audio/mp4' => ['m4a', 'mp4a', 'f4a'], - 'audio/mpeg' => ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'], + 'audio/mpeg' => ['mp3', 'mpga', 'mp2', 'mp2a', 'm2a', 'm3a'], 'audio/mpegurl' => ['m3u', 'm3u8', 'vlc'], 'audio/ogg' => ['oga', 'ogg', 'spx', 'opus'], 'audio/prs.sid' => ['sid', 'psid'], diff --git a/src/Symfony/Component/Mime/Part/DataPart.php b/src/Symfony/Component/Mime/Part/DataPart.php index c6f1cb9742eff..17e3e3c0d38ef 100644 --- a/src/Symfony/Component/Mime/Part/DataPart.php +++ b/src/Symfony/Component/Mime/Part/DataPart.php @@ -56,6 +56,10 @@ public static function fromPath(string $path, string $name = null, string $conte $contentType = self::$mimeTypes->getMimeTypes($ext)[0] ?? 'application/octet-stream'; } + if (false === is_readable($path)) { + throw new InvalidArgumentException(sprintf('Path "%s" is not readable.', $path)); + } + if (false === $handle = @fopen($path, 'r', false)) { throw new InvalidArgumentException(sprintf('Unable to open path "%s".', $path)); } diff --git a/src/Symfony/Component/Mime/Part/TextPart.php b/src/Symfony/Component/Mime/Part/TextPart.php index a41d91ddec86e..35224ad9d8cd0 100644 --- a/src/Symfony/Component/Mime/Part/TextPart.php +++ b/src/Symfony/Component/Mime/Part/TextPart.php @@ -129,7 +129,7 @@ public function getPreparedHeaders(): Headers if ($this->charset) { $headers->setHeaderParameter('Content-Type', 'charset', $this->charset); } - if ($this->name) { + if ($this->name && 'form-data' !== $this->disposition) { $headers->setHeaderParameter('Content-Type', 'name', $this->name); } $headers->setHeaderBody('Text', 'Content-Transfer-Encoding', $this->encoding); diff --git a/src/Symfony/Component/Mime/Test/Constraint/EmailHeaderSame.php b/src/Symfony/Component/Mime/Test/Constraint/EmailHeaderSame.php index bc7e330e051af..74bdc63c79f71 100644 --- a/src/Symfony/Component/Mime/Test/Constraint/EmailHeaderSame.php +++ b/src/Symfony/Component/Mime/Test/Constraint/EmailHeaderSame.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Mime\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Mime\Header\UnstructuredHeader; use Symfony\Component\Mime\RawMessage; final class EmailHeaderSame extends Constraint @@ -44,7 +45,7 @@ protected function matches($message): bool throw new \LogicException('Unable to test a message header on a RawMessage instance.'); } - return $this->expectedValue === $message->getHeaders()->get($this->headerName)->getBodyAsString(); + return $this->expectedValue === $this->getHeaderValue($message); } /** @@ -54,6 +55,13 @@ protected function matches($message): bool */ protected function failureDescription($message): string { - return sprintf('the Email %s (value is %s)', $this->toString(), $message->getHeaders()->get($this->headerName)->getBodyAsString()); + return sprintf('the Email %s (value is %s)', $this->toString(), $this->getHeaderValue($message)); + } + + private function getHeaderValue($message): string + { + $header = $message->getHeaders()->get($this->headerName); + + return $header instanceof UnstructuredHeader ? $header->getValue() : $header->getBodyAsString(); } } diff --git a/src/Symfony/Component/Mime/Tests/AbstractMimeTypeGuesserTest.php b/src/Symfony/Component/Mime/Tests/AbstractMimeTypeGuesserTest.php index 70e419c847d16..7d5656054acb8 100644 --- a/src/Symfony/Component/Mime/Tests/AbstractMimeTypeGuesserTest.php +++ b/src/Symfony/Component/Mime/Tests/AbstractMimeTypeGuesserTest.php @@ -79,6 +79,15 @@ public function testGuessFileWithUnknownExtension() $this->assertEquals('application/octet-stream', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/mimetypes/.unknownextension')); } + public function testGuessWithDuplicatedFileType() + { + if (!$this->getGuesser()->isGuesserSupported()) { + $this->markTestSkipped('Guesser is not supported'); + } + + $this->assertEquals('application/vnd.openxmlformats-officedocument.wordprocessingml.document', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/test.docx')); + } + public function testGuessWithIncorrectPath() { if (!$this->getGuesser()->isGuesserSupported()) { diff --git a/src/Symfony/Component/Mime/Tests/Fixtures/test.docx b/src/Symfony/Component/Mime/Tests/Fixtures/test.docx new file mode 100644 index 0000000000000..2e86b6fcea03c Binary files /dev/null and b/src/Symfony/Component/Mime/Tests/Fixtures/test.docx differ diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 370fd579f95c1..d1aad4619bc40 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -201,7 +201,7 @@ public function setDefault($option, $value) return $this; } - if (isset($params[0]) && null !== ($type = $params[0]->getType()) && self::class === $type->getName() && (!isset($params[1]) || (null !== ($type = $params[1]->getType()) && Options::class === $type->getName()))) { + if (isset($params[0]) && null !== ($type = $params[0]->getType()) && self::class === $type->getName() && (!isset($params[1]) || (($type = $params[1]->getType()) instanceof \ReflectionNamedType && Options::class === $type->getName()))) { // Store closure for later evaluation $this->nested[$option][] = $value; $this->defaults[$option] = []; @@ -532,6 +532,7 @@ public function addNormalizer(string $option, \Closure $normalizer, bool $forceP } if ($forcePrepend) { + $this->normalizers[$option] = $this->normalizers[$option] ?? []; array_unshift($this->normalizers[$option], $normalizer); } else { $this->normalizers[$option][] = $normalizer; @@ -1220,7 +1221,7 @@ private function formatOptions(array $options): string private function getParameterClassName(\ReflectionParameter $parameter): ?string { - if (!($type = $parameter->getType()) || $type->isBuiltin()) { + if (!($type = $parameter->getType()) instanceof \ReflectionNamedType || $type->isBuiltin()) { return null; } diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 8e3c1d3331cd5..1fa0e54645eec 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -1506,6 +1506,17 @@ public function testForcePrependNormalizerClosure() $this->assertEquals(['foo' => '2nd-normalized-1st-normalized-bar'], $this->resolver->resolve()); } + public function testForcePrependNormalizerForResolverWithoutPreviousNormalizers() + { + // defined by superclass + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addNormalizer('foo', function (Options $options, $value) { + return '1st-normalized-'.$value; + }, true); + + $this->assertEquals(['foo' => '1st-normalized-bar'], $this->resolver->resolve()); + } + public function testAddNormalizerFailsIfUnknownOption() { $this->expectException('Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException'); diff --git a/src/Symfony/Component/OptionsResolver/composer.json b/src/Symfony/Component/OptionsResolver/composer.json index d9a237e8ac75a..e5721c670460f 100644 --- a/src/Symfony/Component/OptionsResolver/composer.json +++ b/src/Symfony/Component/OptionsResolver/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "autoload": { "psr-4": { "Symfony\\Component\\OptionsResolver\\": "" }, diff --git a/src/Symfony/Component/Process/Pipes/WindowsPipes.php b/src/Symfony/Component/Process/Pipes/WindowsPipes.php index c548092c51fff..5ebd5ff3c5524 100644 --- a/src/Symfony/Component/Process/Pipes/WindowsPipes.php +++ b/src/Symfony/Component/Process/Pipes/WindowsPipes.php @@ -56,6 +56,9 @@ public function __construct($input, bool $haveReadSupport) $file = sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); if (!$h = fopen($file.'.lock', 'w')) { + if (file_exists($file.'.lock')) { + continue 2; + } restore_error_handler(); throw new RuntimeException('A temporary file could not be opened to write the process output: '.$lastError); } diff --git a/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php b/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php index 6c3decc53cfda..b87782526a1c2 100644 --- a/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php +++ b/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php @@ -41,9 +41,9 @@ public function testFindArguments() $f = new PhpExecutableFinder(); if ('phpdbg' === \PHP_SAPI) { - $this->assertEquals($f->findArguments(), ['-qrr'], '::findArguments() returns phpdbg arguments'); + $this->assertEquals(['-qrr'], $f->findArguments(), '::findArguments() returns phpdbg arguments'); } else { - $this->assertEquals($f->findArguments(), [], '::findArguments() returns no arguments'); + $this->assertEquals([], $f->findArguments(), '::findArguments() returns no arguments'); } } diff --git a/src/Symfony/Component/Process/composer.json b/src/Symfony/Component/Process/composer.json index e0174de75533c..4a7d8f4bf5b94 100644 --- a/src/Symfony/Component/Process/composer.json +++ b/src/Symfony/Component/Process/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" }, diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 5c9954328c7ec..383b364183e57 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -432,8 +432,9 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid // handle uninitialized properties in PHP >= 7.4 if (\PHP_VERSION_ID >= 70400 && preg_match('/^Typed property ([\w\\\]+)::\$(\w+) must not be accessed before initialization$/', $e->getMessage(), $matches)) { $r = new \ReflectionProperty($matches[1], $matches[2]); + $type = ($type = $r->getType()) instanceof \ReflectionNamedType ? $type->getName() : (string) $type; - throw new AccessException(sprintf('The property "%s::$%s" is not readable because it is typed "%s". You should initialize it or declare a default value instead.', $r->getDeclaringClass()->getName(), $r->getName(), $r->getType()->getName()), 0, $e); + throw new AccessException(sprintf('The property "%s::$%s" is not readable because it is typed "%s". You should initialize it or declare a default value instead.', $r->getDeclaringClass()->getName(), $r->getName(), $type), 0, $e); } throw $e; diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php index 0ae762bc0b6f0..cbabce9eb6c27 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -12,6 +12,7 @@ namespace Symfony\Component\PropertyInfo\Extractor; use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag; use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactoryInterface; use phpDocumentor\Reflection\Types\Context; @@ -88,10 +89,12 @@ public function getShortDescription($class, $property, array $context = []): ?st } foreach ($docBlock->getTagsByName('var') as $var) { - $varDescription = $var->getDescription()->render(); + if ($var && !$var instanceof InvalidTag) { + $varDescription = $var->getDescription()->render(); - if (!empty($varDescription)) { - return $varDescription; + if (!empty($varDescription)) { + return $varDescription; + } } } @@ -142,7 +145,7 @@ public function getTypes($class, $property, array $context = []): ?array $types = []; /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */ foreach ($docBlock->getTagsByName($tag) as $tag) { - if ($tag && null !== $tag->getType()) { + if ($tag && !$tag instanceof InvalidTag && null !== $tag->getType()) { $types = array_merge($types, $this->phpDocTypeHelper->getTypes($tag->getType())); } } diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index ccb216b624e68..f30bc440e7533 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -235,11 +235,11 @@ private function extractFromMutator(string $class, string $property): ?array } $type = $this->extractFromReflectionType($reflectionType, $reflectionMethod); - if (\in_array($prefix, $this->arrayMutatorPrefixes)) { - $type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $type); + if (1 === \count($type) && \in_array($prefix, $this->arrayMutatorPrefixes)) { + $type = [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $type[0])]; } - return [$type]; + return $type; } /** @@ -255,7 +255,7 @@ private function extractFromAccessor(string $class, string $property): ?array } if ($reflectionType = $reflectionMethod->getReturnType()) { - return [$this->extractFromReflectionType($reflectionType, $reflectionMethod)]; + return $this->extractFromReflectionType($reflectionType, $reflectionMethod); } if (\in_array($prefix, ['is', 'can', 'has'])) { @@ -290,7 +290,7 @@ private function extractFromConstructor(string $class, string $property): ?array } $reflectionType = $parameter->getType(); - return $reflectionType ? [$this->extractFromReflectionType($reflectionType, $constructor)] : null; + return $reflectionType ? $this->extractFromReflectionType($reflectionType, $constructor) : null; } if ($parentClass = $reflectionClass->getParentClass()) { @@ -319,22 +319,29 @@ private function extractFromDefaultValue(string $class, string $property): ?arra return [new Type(static::MAP_TYPES[$type] ?? $type)]; } - private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionMethod $reflectionMethod): Type + private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionMethod $reflectionMethod): array { - $phpTypeOrClass = $reflectionType->getName(); + $types = []; $nullable = $reflectionType->allowsNull(); - if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) { - $type = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true); - } elseif ('void' === $phpTypeOrClass) { - $type = new Type(Type::BUILTIN_TYPE_NULL, $nullable); - } elseif ($reflectionType->isBuiltin()) { - $type = new Type($phpTypeOrClass, $nullable); - } else { - $type = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $reflectionMethod)); + foreach ($reflectionType instanceof \ReflectionUnionType ? $reflectionType->getTypes() : [$reflectionType] as $type) { + $phpTypeOrClass = $reflectionType instanceof \ReflectionNamedType ? $reflectionType->getName() : (string) $type; + if ('null' === $phpTypeOrClass) { + continue; + } + + if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) { + $types[] = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true); + } elseif ('void' === $phpTypeOrClass) { + $types[] = new Type(Type::BUILTIN_TYPE_NULL, $nullable); + } elseif ($type->isBuiltin()) { + $types[] = new Type($phpTypeOrClass, $nullable); + } else { + $types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $reflectionMethod)); + } } - return $type; + return $types; } private function resolveTypeName(string $name, \ReflectionMethod $reflectionMethod): string diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 0d3c32206786e..d352fa12b61f0 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -11,6 +11,8 @@ namespace Symfony\Component\PropertyInfo\Tests\Extractor; +use phpDocumentor\Reflection\DocBlock\StandardTagFactory; +use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag; use phpDocumentor\Reflection\Types\Collection; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; @@ -46,6 +48,26 @@ public function testParamTagTypeIsOmitted() $this->assertNull($this->extractor->getTypes(OmittedParamTagTypeDocBlock::class, 'omittedType')); } + public function invalidTypesProvider() + { + return [ + 'pub' => ['pub', null, null], + 'stat' => ['stat', null, null], + 'foo' => ['foo', $this->isPhpDocumentorV5() ? 'Foo.' : null, null], + 'bar' => ['bar', $this->isPhpDocumentorV5() ? 'Bar.' : null, null], + ]; + } + + /** + * @dataProvider invalidTypesProvider + */ + public function testInvalid($property, $shortDescription, $longDescription) + { + $this->assertNull($this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property)); + $this->assertSame($shortDescription, $this->extractor->getShortDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property)); + $this->assertSame($longDescription, $this->extractor->getLongDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property)); + } + /** * @dataProvider typesWithNoPrefixesProvider */ @@ -94,7 +116,7 @@ public function typesProvider() ['donotexist', null, null, null], ['staticGetter', null, null, null], ['staticSetter', null, null, null], - ['emptyVar', null, null, null], + ['emptyVar', null, $this->isPhpDocumentorV5() ? 'This should not be removed.' : null, null], ]; } @@ -250,6 +272,16 @@ public function testDocBlockFallback($property, $types) { $this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback', $property)); } + + protected function isPhpDocumentorV5() + { + if (class_exists(InvalidTag::class)) { + return true; + } + + return (new \ReflectionMethod(StandardTagFactory::class, 'create')) + ->hasReturnType(); + } } class EmptyDocBlock diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 0fadd46413eec..8a72e99e9bac0 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -219,6 +219,26 @@ public function php71TypesProvider() ]; } + /** + * * @dataProvider php80TypesProvider + * @requires PHP 8 + */ + public function testExtractPhp80Type($property, array $type = null) + { + $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy', $property, [])); + } + + public function php80TypesProvider() + { + return [ + ['foo', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]], + ['bar', [new Type(Type::BUILTIN_TYPE_INT, true)]], + ['timeout', [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT)]], + ['optional', [new Type(Type::BUILTIN_TYPE_INT, true), new Type(Type::BUILTIN_TYPE_FLOAT, true)]], + ['string', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Stringable'), new Type(Type::BUILTIN_TYPE_STRING)]], + ]; + } + /** * @dataProvider defaultValueProvider */ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/InvalidDummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/InvalidDummy.php new file mode 100644 index 0000000000000..0a4cc81f36be8 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/InvalidDummy.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +/** + * @author Martin Rademacher + */ +class InvalidDummy +{ + /** + * @var + */ + public $pub; + + /** + * @return + */ + public static function getStat() + { + return 'stat'; + } + + /** + * Foo. + * + * @param + */ + public function setFoo($foo) + { + } + + /** + * Bar. + * + * @return + */ + public function getBar() + { + return 'bar'; + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php new file mode 100644 index 0000000000000..484498f4a6c0e --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php @@ -0,0 +1,26 @@ +=7.1.3" }, "require-dev": { "symfony/config": "^4.2|^5.0", diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 9d32a133dc5bc..e1324529afae6 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -522,7 +522,7 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null) { try { - if (($parameterType = $parameter->getType()) && !$parameterType->isBuiltin()) { + if (($parameterType = $parameter->getType()) instanceof \ReflectionNamedType && !$parameterType->isBuiltin()) { $parameterClass = $parameterType->getName(); new \ReflectionClass($parameterClass); // throws a \ReflectionException if the class doesn't exist diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index dbf59c141f830..aeafac4a7e891 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -230,8 +230,9 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s".', $type, $class)); } - $class = $mappedClass; - $reflectionClass = new \ReflectionClass($class); + if ($mappedClass !== $class) { + return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format); + } } return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 01b8950cb4c98..1b6e1b0a06fea 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; use Symfony\Component\Serializer\Mapping\ClassMetadata; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -235,6 +236,43 @@ public function hasMetadataFor($value): bool $this->assertInstanceOf(DummySecondChildQuux::class, $normalizedData->quux); } + public function testDenormalizeWithNestedDiscriminatorMap() + { + $classDiscriminatorResolver = new class() implements ClassDiscriminatorResolverInterface { + public function getMappingForClass(string $class): ?ClassDiscriminatorMapping + { + switch ($class) { + case AbstractDummy::class: + return new ClassDiscriminatorMapping('type', [ + 'foo' => AbstractDummyFirstChild::class, + ]); + case AbstractDummyFirstChild::class: + return new ClassDiscriminatorMapping('nested_type', [ + 'bar' => AbstractDummySecondChild::class, + ]); + default: + return null; + } + } + + public function getMappingForMappedObject($object): ?ClassDiscriminatorMapping + { + return null; + } + + public function getTypeForMappedObject($object): ?string + { + return null; + } + }; + + $normalizer = new AbstractObjectNormalizerDummy(null, null, null, $classDiscriminatorResolver); + + $denormalizedData = $normalizer->denormalize(['type' => 'foo', 'nested_type' => 'bar'], AbstractDummy::class); + + $this->assertInstanceOf(AbstractDummySecondChild::class, $denormalizedData); + } + /** * Test that additional attributes throw an exception if no metadata factory is specified. */ diff --git a/src/Symfony/Component/Templating/Tests/PhpEngineTest.php b/src/Symfony/Component/Templating/Tests/PhpEngineTest.php index d3409a67b1f6e..4e5d1bebe9772 100644 --- a/src/Symfony/Component/Templating/Tests/PhpEngineTest.php +++ b/src/Symfony/Component/Templating/Tests/PhpEngineTest.php @@ -180,9 +180,9 @@ public function testGlobalsGetPassedToTemplate() $this->loader->setTemplate('global.php', ''); - $this->assertEquals($engine->render('global.php'), 'global variable'); + $this->assertEquals('global variable', $engine->render('global.php')); - $this->assertEquals($engine->render('global.php', ['global' => 'overwritten']), 'overwritten'); + $this->assertEquals('overwritten', $engine->render('global.php', ['global' => 'overwritten'])); } public function testGetLoader() diff --git a/src/Symfony/Component/Translation/Tests/PluralizationRulesTest.php b/src/Symfony/Component/Translation/Tests/PluralizationRulesTest.php index 4b8bdffa237b3..615959d58d4f7 100644 --- a/src/Symfony/Component/Translation/Tests/PluralizationRulesTest.php +++ b/src/Symfony/Component/Translation/Tests/PluralizationRulesTest.php @@ -100,9 +100,9 @@ protected function validateMatrix($nplural, $matrix, $expectSuccess = true) foreach ($matrix as $langCode => $data) { $indexes = array_flip($data); if ($expectSuccess) { - $this->assertEquals($nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms."); + $this->assertCount((int) $nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms."); } else { - $this->assertNotEquals((int) $nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms."); + $this->assertNotCount((int) $nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms."); } } } diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index b404b549198a9..c4c58f054d1a7 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -460,7 +460,7 @@ private function loadFallbackCatalogues(string $locale): void protected function computeFallbackLocales($locale) { if (null === $this->parentLocales) { - $parentLocales = json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true); + $this->parentLocales = json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true); } $locales = []; @@ -473,7 +473,7 @@ protected function computeFallbackLocales($locale) } while ($locale) { - $parent = $parentLocales[$locale] ?? null; + $parent = $this->parentLocales[$locale] ?? null; if ($parent) { $locale = 'root' !== $parent ? $parent : null; diff --git a/src/Symfony/Component/Validator/Constraint.php b/src/Symfony/Component/Validator/Constraint.php index 44efe33fa47f8..cf0a9f17efb87 100644 --- a/src/Symfony/Component/Validator/Constraint.php +++ b/src/Symfony/Component/Validator/Constraint.php @@ -108,7 +108,7 @@ public function __construct($options = null) $defaultOption = $this->getDefaultOption(); $invalidOptions = []; $missingOptions = array_flip((array) $this->getRequiredOptions()); - $knownOptions = get_object_vars($this); + $knownOptions = get_class_vars(static::class); // The "groups" option is added to the object lazily $knownOptions['groups'] = true; diff --git a/src/Symfony/Component/Validator/Tests/ConstraintTest.php b/src/Symfony/Component/Validator/Tests/ConstraintTest.php index 6c481b00888ed..26cc460d39c8a 100644 --- a/src/Symfony/Component/Validator/Tests/ConstraintTest.php +++ b/src/Symfony/Component/Validator/Tests/ConstraintTest.php @@ -13,10 +13,13 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\InvalidOptionsException; use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\ConstraintC; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithStaticProperty; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithTypedProperty; use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithValue; use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithValueAsDefault; @@ -245,4 +248,25 @@ public function testAnnotationSetUndefinedDefaultOption() $this->expectExceptionMessage('No default option is configured for constraint "Symfony\Component\Validator\Tests\Fixtures\ConstraintB".'); new ConstraintB(['value' => 1]); } + + public function testStaticPropertiesAreNoOptions() + { + $this->expectException(InvalidOptionsException::class); + + new ConstraintWithStaticProperty([ + 'foo' => 'bar', + ]); + } + + /** + * @requires PHP 7.4 + */ + public function testSetTypedProperty() + { + $constraint = new ConstraintWithTypedProperty([ + 'foo' => 'bar', + ]); + + $this->assertSame('bar', $constraint->foo); + } } diff --git a/src/Symfony/Component/Validator/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php b/src/Symfony/Component/Validator/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php index 9a2958364df17..45e73fc4341e3 100644 --- a/src/Symfony/Component/Validator/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php +++ b/src/Symfony/Component/Validator/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php @@ -35,12 +35,14 @@ public function testThatConstraintValidatorServicesAreProcessed() $addConstraintValidatorsPass = new AddConstraintValidatorsPass(); $addConstraintValidatorsPass->process($container); + $locator = $container->getDefinition((string) $validatorFactory->getArgument(0)); + $this->assertTrue(!$locator->isPublic() || $locator->isPrivate()); $expected = (new Definition(ServiceLocator::class, [[ Validator1::class => new ServiceClosureArgument(new Reference('my_constraint_validator_service1')), 'my_constraint_validator_alias1' => new ServiceClosureArgument(new Reference('my_constraint_validator_service1')), Validator2::class => new ServiceClosureArgument(new Reference('my_constraint_validator_service2')), ]]))->addTag('container.service_locator')->setPublic(false); - $this->assertEquals($expected, $container->getDefinition((string) $validatorFactory->getArgument(0))); + $this->assertEquals($expected, $locator->setPublic(false)); } public function testAbstractConstraintValidator() diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithStaticProperty.php b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithStaticProperty.php new file mode 100644 index 0000000000000..f8b21694089b3 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithStaticProperty.php @@ -0,0 +1,10 @@ +child; + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php index 8eb76ef3f56b4..48de5820fc106 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Optional; use Symfony\Component\Validator\Constraints\Required; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\Mapping\ClassMetadata; @@ -28,6 +29,7 @@ use Symfony\Component\Validator\Tests\Constraints\Fixtures\ChildA; use Symfony\Component\Validator\Tests\Constraints\Fixtures\ChildB; use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Fixtures\EntityParent; use Symfony\Component\Validator\Tests\Fixtures\EntityWithGroupedConstraintOnMethods; use Symfony\Component\Validator\Validator\RecursiveValidator; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -144,6 +146,31 @@ public function testGroupedMethodConstraintValidateInSequence() $this->assertInstanceOf(IsTrue::class, $violations->get(1)->getConstraint()); } + public function testValidConstraintOnGetterReturningNull() + { + $metadata = new ClassMetadata(EntityParent::class); + $metadata->addGetterConstraint('child', new Valid()); + + $this->metadataFactory->addMetadata($metadata); + + $violations = $this->validator->validate(new EntityParent()); + + $this->assertCount(0, $violations); + } + + public function testNotNullConstraintOnGetterReturningNull() + { + $metadata = new ClassMetadata(EntityParent::class); + $metadata->addGetterConstraint('child', new NotNull()); + + $this->metadataFactory->addMetadata($metadata); + + $violations = $this->validator->validate(new EntityParent()); + + $this->assertCount(1, $violations); + $this->assertInstanceOf(NotNull::class, $violations->get(0)->getConstraint()); + } + public function testAllConstraintValidateAllGroupsForNestedConstraints() { $this->metadata->addPropertyConstraint('data', new All(['constraints' => [ diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 1f0dd842ba88b..fb321ff8388d6 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -637,6 +637,10 @@ private function validateGenericNode($value, $object, ?string $cacheKey, ?Metada if ($value instanceof LazyProperty) { $value = $value->getPropertyValue(); + + if (null === $value) { + return; + } } if (\is_array($value)) { diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php index c301df362259b..81a2da7298cfe 100644 --- a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php @@ -97,7 +97,7 @@ public static function castType(\ReflectionType $c, array $a, Stub $stub, $isNes $prefix = Caster::PREFIX_VIRTUAL; $a += [ - $prefix.'name' => $c->getName(), + $prefix.'name' => $c instanceof \ReflectionNamedType ? $c->getName() : (string) $c, $prefix.'allowsNull' => $c->allowsNull(), $prefix.'isBuiltin' => $c->isBuiltin(), ]; @@ -182,7 +182,7 @@ public static function castFunctionAbstract(\ReflectionFunctionAbstract $c, arra if (isset($a[$prefix.'returnType'])) { $v = $a[$prefix.'returnType']; - $v = $v->getName(); + $v = $v instanceof \ReflectionNamedType ? $v->getName() : (string) $v; $a[$prefix.'returnType'] = new ClassStub($a[$prefix.'returnType']->allowsNull() ? '?'.$v : $v, [class_exists($v, false) || interface_exists($v, false) || trait_exists($v, false) ? $v : '', '']); } if (isset($a[$prefix.'class'])) { @@ -244,7 +244,7 @@ public static function castParameter(\ReflectionParameter $c, array $a, Stub $st ]); if ($v = $c->getType()) { - $a[$prefix.'typeHint'] = $v->getName(); + $a[$prefix.'typeHint'] = $v instanceof \ReflectionNamedType ? $v->getName() : (string) $v; } if (isset($a[$prefix.'typeHint'])) { @@ -320,10 +320,14 @@ public static function getSignature(array $a) foreach ($a[$prefix.'parameters']->value as $k => $param) { $signature .= ', '; if ($type = $param->getType()) { - if (!$param->isOptional() && $param->allowsNull()) { - $signature .= '?'; + if (!$type instanceof \ReflectionNamedType) { + $signature .= $type.' '; + } else { + if (!$param->isOptional() && $param->allowsNull()) { + $signature .= '?'; + } + $signature .= substr(strrchr('\\'.$type->getName(), '\\'), 1).' '; } - $signature .= substr(strrchr('\\'.$type->getName(), '\\'), 1).' '; } $signature .= $k; diff --git a/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php b/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php index 78b5aab7556af..5a7c4285252cb 100644 --- a/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php @@ -22,6 +22,11 @@ */ class ResourceCaster { + /** + * @param \CurlHandle|resource $h + * + * @return array + */ public static function castCurl($h, array $a, Stub $stub, $isNested) { return curl_getinfo($h); diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php index 6ecf36695586a..d7c35d2bd1cce 100644 --- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php @@ -144,7 +144,9 @@ abstract class AbstractCloner implements ClonerInterface 'Ds\Pair' => ['Symfony\Component\VarDumper\Caster\DsCaster', 'castPair'], 'Symfony\Component\VarDumper\Caster\DsPairStub' => ['Symfony\Component\VarDumper\Caster\DsCaster', 'castPairStub'], + 'CurlHandle' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castCurl'], ':curl' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castCurl'], + ':dba' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'], ':dba persistent' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'], ':gd' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castGd'], diff --git a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php index b146fc8e6e797..7b7256f8a7f15 100644 --- a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php @@ -287,7 +287,8 @@ public function enterHash(Cursor $cursor, $type, $class, $hasChild) } elseif (Cursor::HASH_RESOURCE === $type) { $prefix = $this->style('note', $class.' resource', $attr).($hasChild ? ' {' : ' '); } else { - $prefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? $this->style('note', 'array:'.$class, $attr).' [' : '['; + $unstyledPrefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? 'array:'.$class : ''; + $prefix = $this->style('note', $unstyledPrefix, $attr).($unstyledPrefix ? ' [' : '['); } if (($cursor->softRefCount || 0 < $cursor->softRefHandle) && empty($attr['cut_hash'])) { diff --git a/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php b/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php index 7c2c058a02d56..a85bc0e85bcf0 100644 --- a/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\AbstractDumper; use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Test\VarDumperTestTrait; use Twig\Environment; @@ -517,6 +518,57 @@ public function testIncompleteClass() ); } + public function provideDumpArrayWithColor() + { + yield [ + ['foo' => 'bar'], + 0, +<< "\e[1;38;5;113mbar\e[0;38;5;208m"\e[m +\e[0;38;5;208m]\e[m + +EOTXT + ]; + + yield [[], AbstractDumper::DUMP_LIGHT_ARRAY, "\e[0;38;5;208m\e[38;5;38m\e[0;38;5;208m[]\e[m\n"]; + + yield [ + ['foo' => 'bar'], + AbstractDumper::DUMP_LIGHT_ARRAY, + << "\e[1;38;5;113mbar\e[0;38;5;208m"\e[m +\e[0;38;5;208m]\e[m + +EOTXT + ]; + + yield [[], 0, "\e[0;38;5;208m\e[38;5;38m\e[0;38;5;208m[]\e[m\n"]; + } + + /** + * @dataProvider provideDumpArrayWithColor + */ + public function testDumpArrayWithColor($value, $flags, $expectedOut) + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows console does not support coloration'); + } + + $out = ''; + $dumper = new CliDumper(function ($line, $depth) use (&$out) { + if ($depth >= 0) { + $out .= str_repeat(' ', $depth).$line."\n"; + } + }, null, $flags); + $dumper->setColors(true); + $cloner = new VarCloner(); + $dumper->dump($cloner->cloneVar($value)); + + $this->assertSame($expectedOut, $out); + } + private function getSpecialVars() { foreach (array_keys($GLOBALS) as $var) { diff --git a/src/Symfony/Component/VarExporter/composer.json b/src/Symfony/Component/VarExporter/composer.json index f589cde09381a..7d4570ce21053 100644 --- a/src/Symfony/Component/VarExporter/composer.json +++ b/src/Symfony/Component/VarExporter/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "require-dev": { "symfony/var-dumper": "^4.4.9|^5.0.9" diff --git a/src/Symfony/Contracts/CHANGELOG.md b/src/Symfony/Contracts/CHANGELOG.md index f909b4976f64b..f3e27978116cd 100644 --- a/src/Symfony/Contracts/CHANGELOG.md +++ b/src/Symfony/Contracts/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +1.1.9 +----- + + * fixed compat with PHP 8 + 1.1.0 ----- diff --git a/src/Symfony/Contracts/Cache/composer.json b/src/Symfony/Contracts/Cache/composer.json index ee73315d755c8..97eabec6a74f1 100644 --- a/src/Symfony/Contracts/Cache/composer.json +++ b/src/Symfony/Contracts/Cache/composer.json @@ -29,6 +29,10 @@ "extra": { "branch-alias": { "dev-master": "1.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } } } diff --git a/src/Symfony/Contracts/EventDispatcher/composer.json b/src/Symfony/Contracts/EventDispatcher/composer.json index 55802a491da69..862c2565f08f1 100644 --- a/src/Symfony/Contracts/EventDispatcher/composer.json +++ b/src/Symfony/Contracts/EventDispatcher/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "suggest": { "psr/event-dispatcher": "", @@ -29,6 +29,10 @@ "extra": { "branch-alias": { "dev-master": "1.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } } } diff --git a/src/Symfony/Contracts/HttpClient/composer.json b/src/Symfony/Contracts/HttpClient/composer.json index 4dc9b2d38ce1e..2382aa7a02ef3 100644 --- a/src/Symfony/Contracts/HttpClient/composer.json +++ b/src/Symfony/Contracts/HttpClient/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "suggest": { "symfony/http-client-implementation": "" @@ -28,6 +28,10 @@ "extra": { "branch-alias": { "dev-master": "1.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } } } diff --git a/src/Symfony/Contracts/Service/ServiceLocatorTrait.php b/src/Symfony/Contracts/Service/ServiceLocatorTrait.php index 0b4d60affa5e1..1737f50e997ab 100644 --- a/src/Symfony/Contracts/Service/ServiceLocatorTrait.php +++ b/src/Symfony/Contracts/Service/ServiceLocatorTrait.php @@ -87,7 +87,7 @@ public function getProvidedServices(): array } else { $type = (new \ReflectionFunction($factory))->getReturnType(); - $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').$type->getName() : '?'; + $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?'; } } } diff --git a/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php b/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php index 5d9d456d0c6d1..82fb5ab361559 100644 --- a/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php +++ b/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php @@ -40,7 +40,7 @@ public static function getSubscribedServices(): array } if (self::class === $method->getDeclaringClass()->name && ($returnType = $method->getReturnType()) && !$returnType->isBuiltin()) { - $services[self::class.'::'.$method->name] = '?'.$returnType->getName(); + $services[self::class.'::'.$method->name] = '?'.($returnType instanceof \ReflectionNamedType ? $returnType->getName() : $type); } } diff --git a/src/Symfony/Contracts/Service/composer.json b/src/Symfony/Contracts/Service/composer.json index 293734edfb900..58748f3740abf 100644 --- a/src/Symfony/Contracts/Service/composer.json +++ b/src/Symfony/Contracts/Service/composer.json @@ -29,6 +29,10 @@ "extra": { "branch-alias": { "dev-master": "1.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } } } diff --git a/src/Symfony/Contracts/Translation/composer.json b/src/Symfony/Contracts/Translation/composer.json index 09749d35f585a..7a449e0ebf7e9 100644 --- a/src/Symfony/Contracts/Translation/composer.json +++ b/src/Symfony/Contracts/Translation/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "suggest": { "symfony/translation-implementation": "" @@ -28,6 +28,10 @@ "extra": { "branch-alias": { "dev-master": "1.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } } } diff --git a/src/Symfony/Contracts/composer.json b/src/Symfony/Contracts/composer.json index 4ac096ed11c0e..f823bf30d6fc9 100644 --- a/src/Symfony/Contracts/composer.json +++ b/src/Symfony/Contracts/composer.json @@ -45,6 +45,7 @@ ] }, "minimum-stability": "dev", + "version": "1.99", "extra": { "branch-alias": { "dev-master": "1.1-dev"